Demystifying Java Generics — gotchas and workarounds

Oliver Keating
Engineering at Alfa
7 min readMay 1, 2019
Credit: Pexels

Java Generics — a compromise

When generics were introduced in Java 5, a compromise was made to make the implementation backwards-compatible with existing code.

Prior to generics, a plain List always used Object, and developers would have to cast with each fetch, and “be careful” to ensure a List of Strings did not accidentally have an Integer inserted.

Comparing different approaches:

  • C++ uses “templates”, literally template code, compiling new code for every type used. Advantage: fully type safe and can support primitives Disadvantage: creates code bloat and more difficult to maintain
  • C# uses fully typesafe (compile and runtime) checking. Advantage: avoids a lot of the pitfalls encountered in java Disadvantage: backwards-incompatible. Microsoft had to effectively write a new collections framework from scratch
  • Java uses “compile-only” checking. Advantage: can be retrofitted and is backwards compatible. Disadvantage: not as strong as could be, has a lot of consequences discussed below

The merits of Sun’s decision can be debated forever, but the key point is it is a design compromise. The main aim of generics was to introduce type safety when inserting object(s) into a container, much in the same way an array will only allow objects of its declared type.

The point of most confusion is the wildcard “?” whose meaning is “an unknown type”. It also specifies “there is a type, but it is unknown”. Previously we could just use Object for an unknown type, but the wildcard is subtly different. I will try to explain.

Adding a String to a List<Integer>

As a result of the lack of run-time checking, it is possible to “damage” a list and actually have a wrong type inserted — albeit with a compile warning:

List<Integer> integerList = new ArrayList<Integer>();integerList.add(1);integerList.add(2);// we cannot directly cast List<Integer> to List<String> as the compiler will spot that is impossible, but we can trick it:List<?> unknownList = integerList; 
// ok because any list can be assigned to List<?>
List<String> stringList = (List<String>) unknownList;
// gives a "Unchecked cast" warning
stringList.add("I don't belong");
// still a reference to the original integerList
for (Integer integer : integerList) {
System.out.println(integer);
}

This is naughty code (not for repeating!) but illustrates the problem. It will print out 1, 2 and then throw a ClassCastException when it tries to fetch the third item (“I don’t belong”) and cast it to an Integer.

This would never be possible with arrays:

Integer[] integerArr = new Integer[10];Object[] objectArr = integerArr;String[] stringArr = (String[]) objectArr; // throws a ClassCastException, but compiles without error or warning

When you cast a generic type from one value to another, unlike a normal cast, at runtime it is effectively a no-op. That is why the compiler warns it is “Unchecked”, literally there is no check that the operation is valid, and will silently proceed.

This means that the compiler is the only point which conducts the type checking — and it needs to be a lot more strict than dealing with arrays because there will be no more runtime checks. Type safety is also only guaranteed if you do not do anything that creates an “Unchecked cast” warning.

Why a List<Integer> cannot be assigned to a List<Number>

This is probably the most difficult aspects of Java generics. Given an Integer is a Number, you might expect an Integer[] to be assignable to a Number[] and that is correct. It would be reasonable to think a List<Integer> is assignable to a List<Number>, but no, not quite.

The problem comes because the compiler has to replicate the type safety of arrays without any runtime checks. To illustrate:

public void testArrayAssignment() {
Integer[] integerArr = new Integer[10];
accept(integerArr);
}
private void accept(Number[] numberArr) {
numberArr[0] = new Double(1.5); // throws ArrayStoreException
}

The accept method takes in an array of Numbers, and is provided with an array of Integer. Then we try to store a Double, which fails with an ArrayStoreException because the runtime type of the array is Integer, and a Double is the wrong object type.

If we replicate this with a parameterised List — then remember, it is not possible (at runtime) for a general List to check if the type being stored is compatible. It is up to the compiler, and that will throw a compile error:

public void testListAssignment() {
List<Integer> integerList = new ArrayList<Integer>();
accept(integerList); // this line fails to compile
}
private void accept(List<Number> numberList) {
// compiles ok - can add any subclass of Number, no runtime checks
numberList.add(new Double(1.5));
}

Using the wildcard — <? extends SomeClass>

Now we can re-write the method to use the wildcard syntax — <? extends Number>:

public void testListAssignment() {
List<Integer> integerList = new ArrayList<Integer>();
accept(integerList); // this now compiles
}

private void accept(List<? extends Number> numberList) {
numberList.add(new Double(1.5)); // this now fails to compile
}

We’ve moved the compile failure to a different point. The accept method signature means “a list of some unknown type that extends (or is) Number”. It will now accept any List whose generic type is any class that extends Number, or is Number itself.

The line where we attempt to add a value will not compile, because the compiler is unable to determine if the added object is of the correct type, because the type is unknown (?). It still doesn’t compile because if it did then we would be able to store a Double in the List<Integer> which is what we wanted to avoid.

In fact, even if we try to add an Integer it will not compile, or anything at all. This gives us some rules:

  • You cannot add anything to a List<?>
  • You cannot add anything to a List<? extends SomeClass>
  • Retrieved objects from List<? extends SomeClass> are of type SomeClass

In effect, a wildcard or wildcard extending something else — the list becomes read-only. This is a direct consequence of the lack of runtime checks.

The only thing you can do that is useful is extract the values — if it is List<? extends SomeClass> then all the values are guaranteed to be at least of the type SomeClass — so this will work:

private void accept(List<? extends Number> numberList) {
for (Number number : numberList) {
System.out.println(number.doubleValue());
}
}

Using the wildcard — <? super SomeClass>

This means “some unknown type, that is SomeClass or a super class of SomeClass”. To demonstrate:

public void testListAssignment() {
List<Number> numberList = new ArrayList<Number>();
accept(numberList);
}
private void accept(List<? super Integer> inputList) {
inputList.add(new Integer(1)); // guaranteed to be fine
Object object = inputList.get(0);// return type is Object
inputList.add(new Object()); // compile failure - unbounded type
}

Operationally, this works the opposite way around to <? extends Number> — you can add but retrieve operations are limited to returning Object.

The method will accept a List<Object>, a List<Number> and a List<Integer> — in all cases you can safely add an Integer, but not a Number or an Object. In all three cases, the only thing you can say about what the get(…) method returns is that it is always assignable to Object.

This gives us the rules:

  • you can add SomeClass instances to List<? super SomeClass>
  • retrieved objects from List<? super SomeClass> are of type Object

More generally if you have a producer of SomeClass (or children), and a consumer of SomeClass (or children) that has a method like the above “accept”, then the rules of thumb to remember:

  • producer takes <? super SomeClass>
  • consumer takes <? extends SomeClass>

Notes about wildcards:

  • If the producer instead returns a list, then please, return it without any wildcards — methods that return wildcards should be discouraged as they will end up “polluting” code. The scope of a wildcard should be kept to a minimum
  • The syntax for <? super SomeClass> and <? extends SomeClass> is inclusive — in both cases SomeClass itself is a legal type
  • The word “extends” applies also when a class implements an interface, i.e. <? extends MyInterface> and not <? implements MyInterface>

Generics Gotchas

It is worth noting that the Java API implements generics in a minimalist way. The collections framework still accepts an Object for methods like contains(…), remove(…) and even Map.get(Object key). Generic types have only been applied where objects are being inserted, and guard against the wrong type being inserted.

Part of this is because of the difficulty encountered in proving to the compiler the type is correct. In trivial cases it may seem obvious, but when you use those methods from another generic class, that might be an “S extends T” or something, things can quickly get out of hand. It may be possible to find a solution that compiles but it may end up looking very ugly. The golden rule is:

  • don’t overuse generics — they may not be able to do what you want

Generics Reflection

Reflection blurs the line between compile and runtime — generic information is available through reflection, but may be confusing!

If you have a method that returns a List, can you determine what the list is of? Because the retro-fit of generics had to be backwards-compatible, the retro-fit to the reflection framework also had to be backwards-compatible too. To avoid breaking existing code, this meant for a lot of reflection methods, a new corresponding “generic” method had to be added. e.g.

In each case, if the latter method is used and the class is not generic, then the result will be identical — a Class object is returned.

There can be some confusion because a Class can have generic types — but calling Class.getTypeParameters() is unlikely to give what you expect. Only one Class object exists in the JVM for every actual class file, and only contains details about that class. The List class declaration is List<E> — where the E is represented by reflection as a TypeVariable, and that is what you will get — not very useful!

The generic methods are defined to return a Type — somewhat awkward to deal with as it requires instanceof checking followed by a cast in order to deal with correctly.

--

--