An overview of generics in Java
Simple class hierarchy for examples:
Example generic interface:
Example generic method:
T extends Animal is a type bound.
What if you need something representing any kind of list of animals?
Reason why this fails: a proper
List<Animal> allows adding any
Animal, while a
List<Dog> should only allow adding
Dogs. This means that the two types are not compatible.
If we only care about the fact that our List contains some kind of
Animals, we can use type wildcards to define this:
? extends Animal is called a subtype wildcard.
? super Dog is called a supertype wildcard
For very generic code, you can also use a wildcard (
? without type bounds)
A note about arrays
We saw above that it is not possible to assign a
List<Dog> to a
List<Animal>, which makes sense. However, this is not the case for arrays. You can easily assign a
Dog array to an
Animal array without the compiler complaining. However, if you then attempt to insert an
Animal that is not a
Dog into the array, you will get an error at runtime.
Generics inside the Java Virtual Machine
Java only added generics in version 1.5. Before that, instead of the generic
ArrayList<T>, there was just the class
ArrayList. When introducing generics, the Java team decided to maintain compatibility by actually erasing the generic type information at compile time, meaning that the byte code running in the Java Virtual Machine does not know anything about generics.
Example generic type:
The above type is compiled into the following raw type:
Before erasing the types, the compiler checks for errors involving generic types. For example, it will forbid wrapping an
Animal in an
AnimalWrapper<Dog>. This means that, although the types are erased later on, the type variables are still respected.
Although the compiler checks for generic type mismatches, that this in itself is not always enough. An example is the following code:
The above code generates warnings, but if you choose to ignore those, you now have an
Animal sitting inside a
List<Dog>. This is known as heap pollution. But what about type safety?
Java handles this in the compiler by inserting a cast whenever the code reads from an expression with erased type. This means that, while we can add the
Animal to our
List<Dog>, we will get a
ClassCastException if we try to retrieve that
Animal as a
Compiled code is equivalent to this:
when we get the
ClassCastException on the last line, that does not help us to find the actual source of the problem (which is the code where we inserted an
Animal inside a
List<Dog>). When debugging such problem, it can be useful to use a checked view of the
List. This checks the type of inserted objects as they are inserted.
Note the use of
Class<Dog> instance which is needed to know the actual value of the type parameter for the
List at runtime.
In some cases, basic type erasure would lead to problems with method overriding. In order to prevent this, the Java compiler sometimes generates bridge methods.
Now, let's say we use it this way:
After erasure, we have
ArrayList. The last line of the code above calls the
add method on
ArrayList. We expect the
add method from
GoodBoyList to override this, but the problem is the method signatures are different.
The compiler solves this by inserting a bridge method
add(Object) into the
GoodBoyList class. That method looks like this:
Bridge methods can also be used when the return type varies. For example, imagine that our
GoodBoyList also overrides the
Note: inside the Java Virtual Machine, a method is defined by its name, the number and types of its arguments and by its return type. This means that, after erasure, we again need a bridge method to make overriding work here. This way, we get two
get methods in
Dog get(int): this is the actual method as defined in
Object get(int): this is a generated bridge method that overrides the
Object get(int)method in
The compiler takes care of the generation of bridge methods, so in principle you don’t have to worry about them. However, they may show up in stack traces or explain why the compiler complains about certain pieces of code.
The Class class
The Java language has a
Class<T> class. A
Class<T> object represent the class
T. This class object can directly be obtained from the class
T. It is also possible to to get a
Class object from an instance of a class, but in that case you are getting the actual run-time type of that instance, which may be a subclass of its compile-time type.
You can use the
Class class to get more information regarding the value of a type variable at run-time (so after type erasure). Example:
T is erased, but we can still have a look at
objectClass to check what kind of object we received.
Class<T> object is also very useful when using
reflection. For example, it can help you access the constructor(s) for the class.
Type arguments cannot be primitives
- A type parameter must always be Object or a subclass of Object. This means that, for example, it is not possible to define an
At runtime, all types are raw
- Reason: type erasure
- Something like
if (object instanceof ArrayList<Dog>) will not compile because this check is impossible to execute at runtime.
- The Class instances that you get are also always raw types. There is no
Type variables cannot be instantiated
- If you have a type variable T, you cannot do
new T[...] (array).
- Reason: type erasure (you would be instantiating the erased value for T, not T itself).
- If you want to construct objects of type T or arrays of type T inside a generic method, you will have to ask the caller for the right object or array constructor or for a Class object.
- While you cannot instantiate an array of type T, you can easily create an
ArrayList<T>. This is because ArrayList is a generic type itself, while in order to create an array of type T we would need the exact type T at runtime.
It’s impossible to create arrays of parameterized types
- You can declare arrays of a parameterized type (e.g.
- You cannot instantiate an array of a parameterized type.
- Reason: type erasure. At runtime, we would just get an
AnimalWrapperarray that allows any kind of
AnimalWrapperwithout throwing an
ArrayStoreException. If that is what you want, you can create an
AnimalWrapperand then cast it to
AnimalWrapper<Dog>(this will generate compiler warnings though).
- The simplest solution is often to just create an
Class type variables are not valid in static contexts
- Type variables defined at the level of the class cannot be used in static contexts (static variables and static methods).
- Example: if you have a class with type parameter T, you cannot have a static variable of type T.
- Reason: type erasure. You can use a class multiple times with different values for T but a static variable only exists once (on the raw type), so it’s impossible to have a static variable with the exact type T for each of those values.
- Remember that you can still use type variables in static contexts if they are not defined at the level of the class. For example, you can have a static method parameterized with type T if that type parameter is declared at the level of the method.
Methods may not clash after erasure
- You are not allowed to declare methods that would clash after erasure (meaning that, after erasure, there would be two methods with the same signature).
- This includes bridge methods! If you get a compiler error about methods clashing after erasure, it’s possible that the clash is generated by the bridge methods generated by the compiler. This is why it’s important to have some understanding of what these bridge methods are.
Exceptions: it is not possible to throw objects of a generic class.
- Reason: type erasure. Catching instances of a generic class with a specific type parameter would require information that is not available at runtime.
- It is still allowed to have a type variable in your throws declaration, as this is checked by the compiler.
- Core Java SE 9 for the Impatient (book by Cay S. Horstmann)