Java Generics: What Beginners Find Surprising

Java generics were introduced in 2004 with Java 5. It has been around for eight years now, but Java generics still pose problems for experienced developers and newcomers alike. Beginners often find Java generics to be counterintuitive. I think it must be difficult to learn Java generics if you don't have the pre-generics perspective first. It's hard to understand some of Java generics' limitations if you don't have a firm grasp of Java code without generics. Experienced developers often have only a rough idea of what Java generics do. They don't want to dig into the details and are often surprised by generics behavior. Today we'll take a look at some of those surprises.

A little bit of history

Let's go back to 2003, when in Java 4, you could write a snippet of code like this:

List l = new List();
l.add("List element");
...
Integer i = (Integer)l.get(0);

(It's been a really long time since I last saw code like this.) Obviously, a runtime exception would be thrown if you tried to run this code. An element l.get(0) is a String and you can't cast a String to Integer.

With generics, the code looks like this:

List<String> l = new List<String>();
l.add("List element");
...
Integer i = (Integer)l.get(0);
Now you get compilation error in line 4. The compiler knows that l.get(0) is a String and you can't cast it to Integer. This code will never run.

 

Backwards compatibility

The thing with Java generics is that you have to think about both versions of the code at the same time. Java generics were introduced late, when Java was a very popular language. The creators of the language decided that Java 5 would be backward-compatible. It had to be:

  • binary compatible - newer versions of JVM are supposed to be able to run old bytecode, compiled for previous versions of JVM. The benefit is you don't have to recompile your code after you upgrade the JVM.
  • source code compatible - the code written for previous version of Java should compile on newer Java as well.
Today we'll examine some generics issues which arise from binary compatibility. In case of Java generics, binary compatibility means that the JVM knows NOTHING about type parameters. The code has two versions: the source code version with type parameters, and the bytecode version where type parameters are erased. The bytecode looks very much like Java 4 version of the code. This means that every object has two types: the compile-time type, with type parameters, and the runtime type, without type parameters. The runtime type may be different from compile-time type.

 

Surprise 1: I can't create an object of parameter type!

This is very surprising for beginners. They think that parameter type behaves very much like any other type and they try to write code like this:

public class MotorcycleFactory<T extends Motorcycle> {
    public T getNewMotorcycle() {
        return new T();
    }
}

There are a couple of reasons why you can't do it in Java.

First, the type parameter T is only present at compile-time. In bytecode it is replaced by its bound, Motorcycle. So the runtime type of new T() is Motorcycle. The compile-time type of new T() is T. In a concrete instance of MotorcycleFactory the type T might be a concrete type, like Kawasaki.

Another reason is that you can't guarantee that type T has a no-argument public constructor. In Java there is no way to specify this requirement. (This is also true for interfaces which I regretted many times. Maybe I'll write a post about it one day.)

Surprise 2: I can't create an array of objects of parameter type

Another surprising piece of code is this:

public class Garage<T extends Vehicle> {
    public T[] vehicles = new T[10];
}
The code does not compile. The error message says Cannot create a generic array of T. A Java array remembers the type of its elements. So an array of type T should know type T. But there is no type T at runtime! If the above code was correct, the type T[] would be erased to its bound type, Vehicle[]. However, elsewhere in the code we could have:
Garage<Car> carGarage = new Garage<Car>();
Car[] cars = carGarage.vehicles;
This code would compile without errors. At runtime carGarage.vehicles would be of type Vehicle[]. We would have a runtime error: carGarage.vehicles type does not match the type of variable cars .

There are even more issues with parametrized arrays and parametrized collections, but I will write about them another day.

What surprised you when you started with Java generics?