Here’s an article about a few of the more subtle aspects of Java 5 Generics. This is hardly the first article about this particular subject, but none of them explain it quite the way I would have wanted to see it when I was wrestling with this issue myself. In fact, some of the blogs I’ve seen on this subject are just plain wrong, stating with great conviction that a collection of type Collection<A>
cannot contain objects of classes derived from A, when the entire key to the mystery consists of understanding that it can! So here’s my attempt to get this thing straight. Enjoy!
Let’s take one of the standard examples of an inheritance tree: we have a superclass Animal, from which we derive subclasses Mammal and Reptile, which are further divided into concrete subclasses such as Cat, Dog and Snake. Now, suppose you want to create a variable pets, containing a collection of Cat, Dog and Snake objects. If you have seen anything about Java generics at all, you will probably guess that the code looks somewhat like this:
Collection<Animal> myPets = new ArrayList<Animal>();
myPets.add(new Dog("Roger"));
myPets.add(new Cat("Miss Fluffy"));
myPets.add(new Snake("Hissy"));
After all, dogs, cats and snakes are all animals, so any collection of animals should be able to accept objects of type Dog, right?
Right! There’s nothing wrong with this at all. It’s just basic object orientation: every Dog object is by definition also an Animal object, and can be used in any situation where an Animal is expected. That includes putting it into a Collection of Animals. C++ programmers may start to protest at this point, but that just demonstrates that C++ is not a very good example of an OO language.
So, if it’s really that simple, why am I making such a big deal out of it? Because once you start working with generics, you sooner or later run into the “? extends” and the slightly less common “? super” syntax, as in this example from the Java 5 run-time library:
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
...
}
And once you’ve seen such an example, uncertainty sets in: “since I want my collection to contain not just Animal objects, but Dog objects and Cat objects as well, shouldn’t I declare my collection as Collection<? extends Animal>
just to be sure?” But as mentioned above, this is not needed. An ordinary Collection<Animal>
will do the job just fine. In fact, you couldn’t create a Collection of Animals which does not also accept Dog objects, even if you wanted to!
So, what is the point of the “? extends” syntax? For that, let’s step back a bit and recall a little bit of trivia about how Java arrays work: the fact that it is possible to trick the compiler into accepting an invalid assignment into an array:
Animal[] dogs = new Dog[10]; // An array of dogs is also an array of animals..
dogs[0] = new Cat("Miss Fluffy"); // ..but this will break at run-time!
This will throw an exception at run-time, of course, but the compiler won’t protest. After all, as far as the compiler knows, dogs is an array of Animals, and a cat is an animal, so what’s the problem? The only way to solve this would have been for the Java designers to disallow even the most innocent-seeming conversions between arrays. Apparently, they decided that price was too high. But now with generics, they had the opportunity to make the whole thing excruciatingly correct for the Collections framework, and so they did.
If you try the following:
Collection<Animal> dogs = new ArrayList<Dog>();
you will get a compile-time error, and the array example above explains why that is exactly as it should be. A list of dogs is not a list of animals, because it does not support everything you should be able to do with a list of animals. Specifically, a list of dogs does not support having cats inserted into it. As far as the Java compiler is concerned, there simply isn’t any kind of inheritance relation between the two collection types at all. And this is where the “? extends” syntax comes in:
Collection<? extends Animal> dogs = new ArrayList<Dog>();
This works, but it’s important to realize exactly what it says. Collection<? extends Animal>
does not mean “a collection of Animal and Animal-derived objects”! Rather, it means “a collection of objects of some unknown class and the derivatives of that class; we don’t know which class but we know that it must be Animal or a subclass of Animal”. The important thing to realize is that the “? extends” syntax modifies the type of the collection object, not the type of its contents.
The even more important thing to realize is that ArrayList<Animal>
is a concrete class, whereas ArrayList<? extends Animal>
is abstract. You cannot do new ArrayList<? extends Animal>()
any more than you can instantiate an abstract class or an interface. The variable dogs, above, may be declared as a collection of “? extends Animal”, but at any moment in time, the object it references will always be a collection of some concrete type — an ArrayList<Dog>
, in this case. Just think of any “? extends” or “? super” construct as a special kind of interface or abstract class, and you’re 99% there.
So, what happens when we try to use this syntax to repeat the bit of array trickery we did above?
Collection<? extends Animal> dogs = new ArrayList<Dog>();
dogs.add(new Snake("Hissy")); // This won't compile..
dogs.add(new Dog("Roger")); // ..and neither will this!
dogs.add(new Animal()); // This won't compile either.
dogs.add(null); // This will work, though.
Because dogs is defined as Collection<? extends Animal>
, the compiler can not make any assumptions about what kind of collection object it will actually point to at run-time. It could be an ArrayList<Goldfish>
, for all the compiler knows at this point! So, adding a Dog or Cat or Snake to the collection would not be safe, and the compiler won’t allow it. The only value that can’t possibly violate the collection’s contract is null, so that’s the only value that is allowed. Try as you might, you will not be able to sneak an object of the wrong type into a collection, without using untyped collections or explicit casts.
2 comments
Very interesting article. The compiler can (and will) make some assumptions about wildcard types though. Perhaps a useful addition to the example:
Collection<? extends Animal> dogs = new ArrayList<Dog>();
dogs.add(new Snake("Hissy")); // This won’t compile..
dogs.add(null); // This will work, though.
Animal animal = dogs.get(0); // ** This will work as well!
"? extends Animal" means "it can be any type, as long as it will fit into an Animal", so assigning the wildcard type to an Animal is safe, and the compiler will allow this. If you write this on the other hand:
Collection<? super Dog> dogs = new ArrayList<Animal>();
dogs.add(new Snake("Hissy")); // This won’t compile again..
dogs.add(new Dog("Roger")); // ** This will work though!
Animal animal = dogs.get(0); // ** won’t work without a cast!
"? super Dog" means "it can be any type, as long as I can fit a Dog in it", so assigning a Dog to the wildcard type is safe. On the other hand, this wildcard type has no meaning when assigning from it, because it could be any supertype of Dog, including Object.
Peter Hendriks
Oops, should have replaced the Collection interface with the List to be able to use the get() method, but I hope you get the point. 🙂
Peter Hendriks