C
that implements an interface I
.While the following is allowed:
I foo( )
{
return new C( );
}
the following is not:
ArrayList<I> foo( )
{
return new ArrayList<C>( );
}
In the first case, callers expect to get an object implementing the interface
I
and therefore it is correct for foo( )
to return an object of class C
. In the second case, callers expect to get an ArrayList
containing objects implementing the interface I
and therefore it should again be correct for foo( )
to return an ArrayList
containing objects of class C
, right?Consider what happens if the compiler were to allow such code to compile. Callers can then add objects of another class
X
, which also implements the interface I
, to the returned ArrayList
with the result that the original ArrayList
, which is only supposed to contain objects of class C
, now also contains objects of an incompatible class X
!A better way to define the second case is:
ArrayList<? extends I> foo( )
{
return new ArrayList<C>( );
}
(You can also return an
ArrayList<I>
instead, but that loosens up the definition of the returned object.)Thanks to Steve for clearing up my muddied thinking.
|
Tweet |
|
Note that this comparable to plain assignments, which is not allowed either:
ReplyDeleteArrayList<I> x = new ArrayList<C>();
The typing rules for assignment are in fact exactly the same as the typing rules for the declared return type and the value of the returned expression. See the JLS Section 14.17: "The Expression must denote a variable or value of some type T, or a compile-time error occurs. The type T must be assignable (§5.2) to the declared result type of the method, or a compile-time error occurs."
Martin is correct in thet return types is equivalent to assignments. However, quite a lot of people I know, and problably Ranjit as well, think that "ArrayList<I> x = new ArrayList<C>();" should work if "I z = new C();" works. The reason that does not is because of the potential problem if you did like this (given two classes C1, and C2, both implementing I):
ReplyDeleteArrayList<C1> x1 = new ArrayList<C1>(); //Legal
ArrayList<I> x2 = x1; // Illegal, equivalent to the return
x2.add(new C2()); // Legal
C1 = x1.get(0); // Legal, but will throw ClassCastException
Hope this clarifies WHY it is illegal, not just that it is.
martin/Anonymous: Yes, this is the same as determining what is "assignable" according to the rules in the JLS. The reason I wanted to point this case out was that it was an instance where the type system deviates from the normal substitutability of a derived class object for a base class object and is somewhat counterintuitive when you consider most folks' understanding of OOP. If a "Cat" is an "Animal", surely I should be able to give you a list of "Cat"-s when you asked me for a list of "Animal"-s!
ReplyDeleteDear Martin
ReplyDeleteIf the list was read-only then that would indeed work. However, all collections in Java are read-write, so if class A returns a list of cats to class B, expecting a list of animals, class B might add a dog to the list, and suddenly it isn't a list of cats any more. It's still a list of animals, so class B is fine, but if class A still has an internal reference to the list, it won't know that the list has become a list of animals instead, so whenever it encounters the dog it will get problems, as a dog is not a cat. The result would have to be a ClassCastException, and suddenly you've lost all benefits of generic lists.
Regards
- Jonno
P.S. BTW, I'm the same as anonymous above