Java generics present many specificities.
Here these I frequently rely on and that I consider as an unavoidable requirement to understand and work with generics.
I will illustrate these points by endeavoring to stay as general as possible about the principles. About the code examples, they will rely on the Java generic collections.
Variable assignments and passing arguments with generic classes and collections
1) A method of a generic class that accepts the generic as parameter may accept as argument any instance of the class (and so subclasses too) of the declared generic type.
1-bis) As a consequence, a generic collection variable may accept to add any instance of the class (and subclass) of the declared generic type :
List<Number> numbers = ...; numbers.add(1L); // Long numbers.add(1F); // Float |
Generics are invariant.
2) Invariant for assignments
A generic collection variable cannot be assigned with a generic collection which the generic type is a subclass of its generic type :
List<Integer> integers = ...; List<Number> numbers = ...; numbers = integers; // doesn't compile |
Conceptually it is not valid because it would break the type safety according to the point 1.
If it would be legal, the two lists would refer the same object. So changing the state of the first one would be reflected in the state of the second one.
For example, if I added a Float object in a List of Number, which is legal (point 1) and then suppose that it would be valid to assign a List of Integer variable to this List of Number, what would it mean ?
It would mean that the list of Integer variable could refer a List of Number containing a Float. We don’t want that as it defeats the generics purpose : the type safety.
Here is the example code :
List<Integer> integers = ...; List<Number> numbers = ...; numbers = integers; // doesn't compile but suppose that it would numbers.add(1F); // valid Integer integer = integers.get(0); // inconsistent result at runtime. I expect to have a Integer but actually I retrieve a Float |
3) Invariant for parameters
A generic collection parameter cannot be valued with a generic collection which the generic type is a subclass of its generic type.
Similarly to what we have seen in the first point, inside a method we could also change the content of an collection object referenced by a variable parameter where the declared type doesn’t have any explicit bound on the generic type such as void doThat(List<Foo> foos)
.
So it has the same constraints as previously.
For example, if we could pass a List of Integer as argument to a method that expects to have a List of Number, it would break the type safety of the List of Integer as the method may add a Float or Long in it. So it is not possible either :
List<Number> numbers = new ArrayList<>(); numbers.add(1); numbers.add(2F); List<Integer> integers = new ArrayList<>(); integers.add(1); addNumberAsNewElement(numbers); // valid addNumberAsNewElement(integers); // doesn't compile to prevent type safety issues ... public void addNumberAsNewElement(List<Number> numbers) { numbers.add(1F); } |
The conclusions for 2) and 3) : Without specifying a wildcard, the compiler expects that the generic type be exactly the same between the assigned type and the variable target of the assignment or between the parameter of the method and the declared type passed to :
List<Integer> integers = ...; List<Integer> otherIntegers= ...; integers = otherIntegers; // valid |
and
public int sumInt(List<Number> numbers){..}; ... List<Number> numbers = ...; int sum = sumInt(numbers); // valid |
However the need to assign/pass as argument a reference of a Collection parameterized with a subtype to a Collection parameterized with a super type is a so common need as generics had to introduce a way to do it : the bounded type parameters.
However, this ability has a constraint : the compiler doesn’t allow to add any type of object in the collection object that is the target of the assignment.
//TO DELETE
In this way, the compiler is ensured that the type safety is respected. We could assign a List of Integer to a List of Number but we could not add anything in the List of number (as for example a Float) so to not break the type of List Of Integer (point 2 and 3).
//TO DELETE
The next points (point 4, 5 and 6) present three ways to accomplish it.
4) Upper bounded wildcard (covariance) to relax the restriction on a generic type.
A generic collection variable can be valued with a generic collection which the declared generic is a subclass of its declared generic (or the same generic) if the generic collection target of the assignment specifies an upper bounded wildcard.
But the use of an upper bounded wildcard also means that we could not pass any object but null in any invoked method accepting the generic type.
In the case of Collection, it means that you cannot add nothing in the Collection but null.
List<Integer> integers = ...; List<? extends Number> numbers = ...; numbers = integers; // valid List<String> strings = ...; numbers = strings ; // doesn't compile as bounded wildcard to Number numbers.add(1L); // doesn't compile as a collection with wildcard generic doesn't allow to add elements in but null |
With method parameters using generic collections, it follows the same logic.
public int sumInt(List<? extends Number> numbers){ ... numbers.add(1L); // doesn't compile as a collection with wildcard generic doesn't allow to add elements in but null } List<Number> numbers = ...; List<Integer> integers = ...; List<String> strings = ...; sumInt(numbers); // valid sumInt(integers); // valid sumInt(strings) // not valid |
5) Unbounded wildcard (covariance) to relax the restriction on a variable referencing a generic type.
Common use : reading a collection.
If we don’t need to set a specific constraint on the generic of the collection target of the assignment, we could use an unbounded wildcard (?) :
List<Integer> integers = ...; List<?> anyObjects = ...; anyObjects = integers; // valid List<String> strings = ...; anyObjects = strings ; // valid |
But this will have the same limitation as the previous point (upper bounded wildcard). Only null element may be used as parameter in the methods accepting the generic type.
6) Lower bounded wildcard (contravariance) to relax the restriction on a variable referencing a generic type.
Common use : modifying a collection.
A generic collection variable can be valued with a generic collection which the declared generic is a super type of its declared generic (or the same generic) if the generic collection target of the assignment specifies an lower bounded wildcard.
Similarly to a generic type (point 1), a variable specifying a lower bounded wildcard may pass as generic any instance of the class specified or of its subclasses in any invoked method accepting the generic type.
Here a sample code that illustrates that :
public int sumInt(List<? super Number> numbers) { ... numbers.add(1L); // valid ... } // Client code List<? super Number> numbers = ...; // adding in the collection numbers.add(1); // valid numbers.add(1L); // valid // variable assignment List<Integer> integers = ...; numbers = integers; // not valid List<Serializable> serializables = ...; numbers = serializables; // valid // invocation sumInt(numbers); sumInt(serializables); sumInt(integers); // not valid as subtype disallowed |
Generics and Arrays : not the same rules.
Assignation/argument passing and adding element rules for generic collections don’t work in the same way for arrays.
7) Generic collections are not reifiable while arrays are. It means that at run time, a compiled List<String> is considered by the JVM as a List instance while a compiled String[] is considered by the JVM as a array of String instance.
It has multiple consequences.
8) Generics collections have a more limited overloading capacity.
This code compiles fine as String[] and Number[] are considered as two distinct types after compilation :
public void method(String[] array){ ... } public void method(Number[] array){ ... } |
But this code doesn’t compile as after compilation and erasure of the generics, the two methods have exactly the same signature. Indeed, List class is used in both.
public void method(List<Number> list){ ... } public void method(List<String> list){ ... } |
After compilation, the code (if it could compile) could look like that :
public void method(List list){ ... } public void method(List list){ ... } |
Which is indeed not valid for the compiler.
9) Arrays are covariant while generics collections are invariant.
For generics, the point 2 and 3 illustrate it.
For arrays, things are different.
At compile time, we can indeed assign an array of a type to a array variable declared with the super type of it.
The check of the validity of the elements stored in the array is performed at runtime.
Number[] numberArray = {1, 5F}; Integer[] integerArray = {1}; numberArray = integerArray; // compile fine numberArray[0] = 5; // fine at runtime as valid type numberArray[0] = 5F; // exception at runtime as Integer[] should not store Float. |
It is the same behavior for method providing as parameter an array.
Integer[] integerArray = {1}; method(integerArray); public void method(Number[] array){ array[0] = 1; // fine at runtime array[0] = 1F; // // exception at runtime as Integer[] should not store Float. } |
10) Arrays may store primitives and objects while generics can store only objects.
int[] myPrimitiveArray = ... // valid Integer[] myObjectArray = ... // valid List<Integer> myObjectList = ...; // valid List<int> myPrimitiveList = ...; // doesn't compile |
It has implications on memory consumption as Integer is stored on more space as its primitive counterpart int.
Erasure
…
Raw types
12) Using raw types has also consequences in terms of inheritancy.
The superclasses (respectively, superinterfaces) of a raw type are the erasures of the superclasses (superinterfaces) of any of the parameterizations of the generic type.