Generics in Java is one of those features where almost everyone uses but very people, including myself, really understand. I had a conversation just today about understanding a use case of generics within collections and came to a realization of why generics were applied in a certain way. It is very important to not only understand the base concept of generics (ie: List strings = new ArrayList();
), but to also understand the subtle differences. Generics are very powerful after all. In particular, there are a few different ways to declare generics within collections and they all appear similar, but have very different meanings. Understanding these subtle differences can help alleviate any misconception as to why your IDE or compiler throws an error or warning in a certain situation.
My conversation today revolved around the notion of generics with wildcards. Generics supports the notion of wildcards in a few different usecases:
List<Number> l = ...; // simple parameterized type
List<? extends Number> l = ...; // wildcard with upper bounds
List<? super Integer> l = ...; // wildcard with lower bounds
List<?> l = ...; // wildcard with no bounds (bounds to Object) |
List<Number> l = ...; // simple parameterized type
List<? extends Number> l = ...; // wildcard with upper bounds
List<? super Integer> l = ...; // wildcard with lower bounds
List<?> l = ...; // wildcard with no bounds (bounds to Object)
The simple parameterized type is self-explanatory and most popular. The latter non-boundary-based wildcard is also pretty self explanatory and commonly used. The wildcard with a lower bounds (? super T
) is rarely used but has specific use cases, none of which I will dig into in this article. However, I do encourage you to investigate what it means and why you might want to use it (hint: the most common use case is comparables such as java.util.Collections.sort
). The upper bounds wildcard, especially as it pertains to collections, is what we are interested in today.
Take the following example:
List<Animal> animals;
List<? extends Animal> animals; |
List<Animal> animals;
List<? extends Animal> animals;
Those two declarations are very similar to each other, but there is a very keen difference. The first case is saying the list will contain a possible mixture of any objects that are or extend from Animal. In other words, you could have a list containing dogs and cats, since both are animals. The second case is saying the list will contain a set of objects whose base extends from animal. That’s almost the same thing, but the difference is that the latter list has the possibility to only contain dogs or only contain cats. The first case is always open to containing a mixture. It all boils down to developer intent. The wildcard is used by a developer to basically say “I know I have a specific type of list, but I’m just not sure what that type might be at this moment”. It allows the developer to ensure that the list will only contain a given type. The first case would allow the developer to put anything in that list. If the intent is to ensure the list can only contain dogs, that would fail as you could put cats into it.
Let’s consider a use case. Consider a factory class that returns a list of a given animal. You may not know what type of animal you will get back, so you can’t type it as a specific animal. However, you still want to ensure that the caller retrieving the list treats it as a specific type and not allowing them to add other animals to it.
public class AnimalFactory {
public static List<? extends Animal> getAnimals(String type) {
// return either List<Dog> or List<Cat> for example
}
public static void main(String[] args) {
List<? extends Animal> animals = getAnimals("dogs");
animals.add(new Cat()); // this fails
}
} |
public class AnimalFactory {
public static List<? extends Animal> getAnimals(String type) {
// return either List<Dog> or List<Cat> for example
}
public static void main(String[] args) {
List<? extends Animal> animals = getAnimals("dogs");
animals.add(new Cat()); // this fails
}
}
This example fails because we attempt to put a cat into a wildcarded animal list. However, if the declaration was simply List
, then we could add a cat just fine. The wildcard type says that the list is typed as something specific, but it’s exact value is not yet known. Because of that reason, Java disallows you to add a cat to the list because that list may in fact be a list of dogs and that would be invalid.
List<Dog> dogs = ...;
dogs.add(new Cat()); // invalid |
List<Dog> dogs = ...;
dogs.add(new Cat()); // invalid
Let’s look at some more commonly misused examples.
List<? extends Animal> animals = new ArrayList<Dog>(); // works
List<Animal> animals = new ArrayList<Dog>(); // fails |
List<? extends Animal> animals = new ArrayList<Dog>(); // works
List<Animal> animals = new ArrayList<Dog>(); // fails
The first example works, because ArrayList
satisfies the wildcard. A list of dogs is a specific type of a list of animals. The second case, however, fails because a list of dogs is not the same thing as a list of animals. The reason is that you could add a cat to the latter list (since a cat is an aminal) and that would deem the list of dogs invalid (a cat is not a dog). The first example you cannot add a cat or a dog, since Java does not know the exact type to allow.
So, how do you add to a list when it is typed as ? extends T
. The simple answer is you don’t and I believe that is one of the rationales and use cases for declaring a list in that manner. If we consider our factory example again, the use case for retrieving the list is most likely to be able to iterate and act upon it, not add or modify it. In that scenario, the following code works just fine since you know the list contains animals, you are just not sure what type. In other words, you can treat the items in the list as some form of animal generally, but you cannot change the list without knowing what the exact type is (in order to maintain the semantics of a specific type being in the list).
List<? extends Animal> animals = AnimalFactory.getAnimals(type);
for (Animal animal : animals) {
animal.speak();
} |
List<? extends Animal> animals = AnimalFactory.getAnimals(type);
for (Animal animal : animals) {
animal.speak();
}
But what if you really, really, really need to update the list. The answer is just type cast it and away you go. After all, types in Java are erased anyways (see my type erasure blog for more information). You may get a Java warning though in doing so, but it’s just warning you that you are taking code into your your own hands and assuming responsibility (ie: possibly casting what at runtime is a list of cats into a list of dogs).
List<? extends Animal> animals = AnimalFactory.getAnimals("cats");
List<Dog> dogs = (List<Dog>) animals;
dogs.add(new Dog());
for (Dog dog : dogs) { // ack...class cast exception because the list was actually List<Cat>
dog.bark();
} |
List<? extends Animal> animals = AnimalFactory.getAnimals("cats");
List<Dog> dogs = (List<Dog>) animals;
dogs.add(new Dog());
for (Dog dog : dogs) { // ack...class cast exception because the list was actually List<Cat>
dog.bark();
}
If you were using arrays, you could not do: Dog[] dogs = (Dog[]) new Cats[] { new Cat(); }
. However, that is essentially what you are saying above. Since types are erased in Java and the list is just a list at runtime, Java cannot really enforce the check as an error and instead gives you a warning stating that you may be converting a list of cats into a list of dogs which would not be possible under the normal rules of the universe.
Let’s look at one final example:
public void process(List<Animal> animals) {
// do stuff
}
public void test() {
List<? extends Animal> animals = new ArrayList<Dog>();
process(animals); // fails
} |
public void process(List<Animal> animals) {
// do stuff
}
public void test() {
List<? extends Animal> animals = new ArrayList<Dog>();
process(animals); // fails
}
This example once again fails. By now you hopefully understand why. Even though you can define the dogs as ? extends Animal
and then declare it as ArrayList
, you still cannot pass that list into a method that expects a certain type.of animals. Even when the list is typed as List, you are still saying you have a list of some type of animal. It may be a list of cats or it may be a list of dogs in this example. The process method, however, is accepting a list that supports both cats and dogs or any other animal. It is almost the same thing but the key difference is that the declaration is saying I have a specific type of animals whereas the method is saying I support all animals. If that does not convince you, put the pieces from earlier together. Within the process method, we saw earlier we could do: animals.add(new Cat())/code>
since cat is an animal. However, we were not able to do the same when the type was ? extends Animal
because we were not guaranteed to have a known type that would be satisfactory to the actual list at runtime. If that is true, then Java cannot allow you to pass in the animals variable to the method because you could add a cat or a dog or whatever to it.
In other words, a List is not necessarily a List extends Animal>. They could be the same, but one is more generic than the other. After all, the following is valid: List extends Animal> animals = new ArrayList
in which case it is the same as List at runtime. The keyword here being runtime. The compiler only works at compile time and all it knows from the definition is that it is a specific type of "something" but what that something is is not known until runtime, so prevent any type of modification to the contents of that "something" that could alter the semantics of the actual runtime declaration.
Hopefully this helped to clarify any misunderstandings between generics in collections and helps you to better utilize the proper form when handling collections. The basic rules are these:
-
Use
? extends T
when you do not need modification of the contents and want to ensure a specific type is always being used. NOTE: this does not
guarantee modification of the contents since you can always type cast (use an unmodifiable collection for that).
-
Use
T
when you are talking about a collection that can handle and contain any type of T and it does not matter if the list contains a dog and a cat
for example.