Explanations for the more confusing aspects of Java Generics.

Photo by Alexander Grey, https://unsplash.com/photos/VmAWsnlVyMo

Generics have been around since 2004, but still tend to confuse even experienced developers from time to time. The basics are easy to grasp and supported by a ton of excellent tutorials, such as the official one from Oracle. The following article focuses on the more confusing aspects of generics, and explains them with simple examples. So the next time you encounter one of those dreaded compiler errors, you’ll be able to solve them with confidence.

Quick links to the sections below:

Example Class Hierarchy

We will be using a very simple class hierarchy in the following examples:

  • A Bike with a String attribute for its color.
  • And extending from this class:
    • EBike
    • Tandem
class Bike {
  String color = "blue";
}

class EBike extends Bike { }

class Tandem extends Bike { }

Aliasing

Aliasing is a basic concept in Java programming that you will probably know already. However, as it is an important prequisite for understanding the rest of this article, it’s good to quickly load it into your mental RAM before diving into actual generics.

Let’s start by creating an EBike instance, which receives the default color “blue”:

EBike eBike_1 = new EBike();
System.out.println(eBike_1.color);
// blue

Now we assign this EBike to another EBike reference, and change the color via this second reference:

EBike eBike_2 = eBike_1;
eBike_2.color = "red";
System.out.println(eBike_2.color);
// red

Since eBike_1 and eBike_2 are references that both point to the same object in memory, the color change on eBike_2 is also visible via eBike_1:

System.out.println(eBike_1.color);
// red

So the first key takeway is this:

There can be multiple references (aliases) to one and the same object. If you change this object through one of these references, you will see the results of the change also through all the other references.

And of course, the EBike instance that we have created before can also be referenced (and modified) using its parent type Bike:

Bike bike = eBike_1;
bike.color = "green";
System.out.println(eBike_1.color);
// green
The references to an object can have a type that is less detailed that the object's actual type (i.e., a supertype).

Generic Lists

As a baseline, let’s begin with the easy stuff: Adding and retrieving elements on “normal” generic lists (i.e., without wildcards). First, we create a list of Bikes:

List<Bike> bikes = new ArrayList<>();

Obviously, we are allowed to add Bikes to this list of Bikes:

bikes.add(new Bike());

And due to their subtype relation, we are also allowed to add EBikes and Tandems:

bikes.add(new EBike());
bikes.add(new Tandem());

So a List<Bike> can potentially hold Bikes and any of its subtypes. This is perfectly fine, because when retrieving elements from this list, we are only guaranteed that it will be a Bike (but not whether the concrete element is something more specific):

Bike bike = bikes.get(1);

Even though element #1 in this list is actually a Tandem, the compiler cannot guarantee it. It only knows that this is a list of Bikes, but not which of its elements might be something more specific:

// "Type mismatch: cannot convert from Bike to Tandem"
Tandem tandem = bikes.get(1);

To guarantee that you really get Bikes out of this list, the compiler won’t let you add any of the following to this list:

  • less detailed types (supertypes of Bike, such as “Object”)
  • other types (such as “String”)
// "not applicable for the arguments (Object)"
bikes.add(new Object());

// "not applicable for the arguments (String)"
bikes.add("my bike");
A List<T> is a list that can contain objects of type T (including subclasses of T).
Hence, you can add T and subclasses of T to this list.
Everything you get from this list is guaranteed to be a T.

Now let’s investigate assignments (aliasing) for generic lists. The following assignment is not allowed:

List<Bike> bikes = new ArrayList<>();
List<Tandem> tandems = new ArrayList<>();

// "cannot convert from List<Bike> to List<Tandem>"
tandems = bikes;

This is quite intuitive, as it resembles the rules that apply to simple assignments without generics:

// "cannot convert from Bike to Tandem"
Bike bike = new Bike();
Tandem tandem = bike;

But interestingly, the reverse is also forbidden:

// "cannot convert from List<Tandem> to List<Bike>"
bikes = tandems;

What’s going on here? After all, we are allowed to do the following:

Tandem tandem = new Tandem();
Bike bike = tandem;

So why can’t we do the same with lists? The answer is easy if we recall from above which types can be added and retrieved from generic lists. Consider the following example:

List<Tandem> tandems = new ArrayList<>();

// if this were allowed...
List<Bike> bikes = tandems;

// ...we could smuggle EBikes into the List<Tandems>
// through its alias List<Bike>...
bikes.add(new EBike());

// ...leading to the following problem:
Tandem tandem = tandems.get(1);
// oops, this is actually an EBike!
You cannot assign a List<S> to a List<T> (or vice versa), even if S is a subclass of T.

Upper-Bounded Wildcards (“? extends”)

In certain cases, the previously mentioned restriction is an obstacle for efficient development. Suppose you want to write a method that receives a list of Bikes, and prints their color:

void printColors(List<Bike> bikes) {
  for (Bike bike : bikes) {
    System.out.println(bike.color);
  }
}

This method compiles, and can be called with a list of Bikes as input:

List<Bike> bikes = ...;
printColors(bikes);

However, when trying to pass a list of Tandems to this method, the compiler complains:

List<Tandem> tandems = ...;
// "printColors(List<Bike>) 
//  is not applicable for the arguments 
//  (List<Tandem>)"
printColors(tandems);

The reason is the same as before: With the given definitions, the method “printColors” could theoretically smuggle EBikes into the provided list of Tandems, resulting in a bad surprise for the method’s caller when later retrieving elements from the list. In this simple example, it is obvious for us that the method only reads from the list, without adding elements to it. But this is not enough to satisfy the compiler.

To overcome this problem, we can instead provide the method with a parameter of type List<? extends Bike>, which we call an upper-bounded wildcard:

void printExt(List<? extends Bike> bikes) {
  for (Bike bike : bikes) {
    System.out.println(bike.color);
  }
}

This type basically says the following: “I might be a List<Bike>, OR a list of any of Bike’s subtypes (List<Tandem>, OR a List<EBike>)”. With this alternative method definition, we can safely pass the corresponding lists as arguments:

List<Tandem> tandems = ...;
printExt(tandems);

List<EBike> eBikes = ...;
printExt(eBikes);

List<Bike> bikes = ...;
printExt(bikes);

However, this flexibility comes at a cost: To prevent printExt from smuggling stuff into the list that doesn’t belong there, it is more or less read-only. In particular, you cannot invoke the add method of a List<? extends Bike>, regardless of what you try to pass as argument to the “add” method:

void forbidden(List<? extends Bike> bikes) {

  // "add(capture#1-of ? extends Bike) 
  //  in the type 
  //  List<capture#1-of ? extends Bike> 
  //  is not applicable for the arguments 
  //  (EBike)"
  bikes.add(new EBike());

  // analogous error here:
  bikes.add(new Bike());

  // and also here:
  bikes.add(new Object());

  // only this is allowed:
  bikes.add(null);

}

With the previous explanations, this restriction is easy to understand: If any of these add operations was allowed, we would get into trouble in one case or another:

  • If we were allowed to add an EBike, callers of this method that have provided a list of Tandems would get mad (“We don’t want E-Bikes, we want Tandems!!!”).
  • Likewise, if we were allowed to add a simple Bike, callers with Tandems would get mad as well (“A normal Bike amongst our precious Tandems???”)
  • And of course, adding some arbitrary Object to this list is also out of the question (“What on earth is THIS???”).
  • The only thing we may add without corrupting the list’s type is null. This might also infuriate callers, but as Java developers, we are used to NullPointerExceptions ;)

”? extends” lists can therefore be regarded as mere producers of data (from the perspective of the method that declares them as parameter). They produce data that we can retrieve with the “get” method, but they can’t consume any data through the “add” method. But be aware that they are not fully read-only. Apart from the already mentioned exception that we may still add null elements, such lists are also modifiable in the following aspects:

  • They can be emptied by invoking the “clear” method.
  • Elements can be deleted through the “remove” method.
  • Retrieved elements can be modified directly (e.g., change Bike colors).

By the way, don’t let the “capture#1-of” strings inside the error message confuse you. They only show how the compiler internally tries to “capture” concrete types in place of wildcards. But as they don’t add any information relevant for solving the problem, you can simply ignore them.

A List<? extends T> might be a List<T>,
or a List<SUB_CLASS_OF_T>
(i.e., a List of anything that extends T).

Everything you get from this list is guaranteed to be a T.

But you are not allowed to add anything to this list (except null),
because we don't know for sure what kind of list it actually is.

Such lists can be regarded as producers of data.

You can assign a List<S> to a List<? extends T>
if S is a subclass of T.

How far upwards can we bump an upper-bounded wildcard? Of course, right until the upper end of Java’s class hierarchy: <? extends Object>. The behavior of this is completely analogous to what we’ve already seen with <? extends Bike>, it’s only much wider. A shorter way to express this is to just write <?>. Some sources on the web claim that there are certain subtle differences between <? extends Object> and <?> with regard to non-refiability (e.g., in combination with instanceof checks). From my observations, these differences do not exist (at least not in Java 17), which is why we won’t go into further details here. In any case, you are good to go with the short form <?>.

You can write List<?> instead of List<? extends Object>.

Lower-Bounded Wildcards (“? super”)

When going to a sports store, we might want to shop more than just bikes, so let’s add one more class to our example hierarchy:

class Product {}

class Bike extends Product { ... }

The following method generously adds three different types of bikes to your shopping cart:

void addBikes(List<Product> cart) {
  cart.add(new Tandem());
  cart.add(new EBike());
  cart.add(new Bike());
}

Invoking this method with a list of Products works fine:

List<Product> products = new ArrayList<>();
addBikes(products);

But it will refuse to do its goodness when you present it with a mere list of Bikes:

List<Bike> bikes = new ArrayList<>();
addBikes(bikes);

We already know why this is forbidden: We cannot risk passing our specific list of Bikes into a method that might add any kind of Product to it. But how to make the method work with both types of input lists, and still stay safe? We somehow have to adapt the method signature such that it says: “Just give me any list that allows me to add Tandems, EBikes, and Bikes”. The following types of lists satisfy this condition:

  • List<Bike>
  • List<Product>
  • List<Object>

This combination can be expressed with a lower-bounded wildcard:

void addBikesSuper(List<? super Bike> bikes) {
  bikes.add(eBike);
  bikes.add(tandem);
  bikes.add(bike);
}

And again, this feature comes with a cost: When retrieving elements from this list, the compiler can only guarantee that it is an Object, nothing more. Altogether, a “super” list can be regarded as the opposite to the “extends” list from before, and primarily serves as a consumer of data (whereas it can only produce low-detail Object output).

A List<? super T> might be a List<T>,
or a List<SUPER_CLASS_OF_T>
(i.e., a List of anything that is a super class of T).

You can add everything up until T to this list.

But you can only get low-detail Objects from this list.

Such lists can be regarded as consumers of data.

You can assign a List<S> to a List<? super T>
if S is a super class of T.

PECS

A common mnemonic for memorizing the behavior of “extends” and “super” is the PECS acronym, which stands for:

Producer Extends, Consumer Super

This refers to our previous observations that “extends” lists serve as (quasi read-only) producers of data, whereas “super” lists can be used as consumers of data.

Type Parameters vs. Type Arguments

What is the difference between a parameter and an argument? Let’s start with a simple method definition:

void say(String greeting) {
    System.out.println(greeting);
}

Here, the “greeting” variable in the method’s declaration is a parameter. When you invoke this method

say("Hello World!");
String hi = "Hi there!";
say(hi)

then both the String “Hello World” and the variable “hi” are used as an argument for the method invocation. If you need a mnemonic for this: For an argument (quarrel), it takes two.

In the context of generics, you can define type parameters:

public class ArrayList<E> { ... }

These type parameters are placeholders that can be populated with different type arguments when you actually instantiate the class:

new ArrayList<Bike>()

Where to use T / ? / extends / super

Another question with confusion potential is this: Which generic syntax elements (“T”, “?”, etc.) can be used where? The answers are easy if we keep the previous clarification between parameters and arguments in mind.

<T>: Type Parameter Definition

With <T> (or any other name instead of T) we can define a type parameter for

  • a class
  • or a method

If needed, we can also provide multiple type parameters:

class Box<T, U, V> { ... }
<T, U, V> void foo() { ... }

Type parameters serve as placeholders for concrete types when the class is instantiated, or when the method is invoked.

<T extends Bike>: Bounded Type Parameter Definition

By using the “extends” keyword, we can provide an upper bound for the type parameter. This means that we may later only provide type arguments that are a subtype of the bounded parameter type.

We can also demand that the type argument satisfies more than one upper bound condition. In the following example, we want it not only to be a Bike, but also to be Serializable and Comparable:

<T extends Bike & Serializable & Comparable>

Note that when using multiple bounds, at most one of them may be a class (namely the first one). Providing more than one class would not make sense, because Java doesn’t allow multiple inheritance.

It is not allowed to use the “super” keyword for a type parameter! Don’t confuse this with the wildcard <? super Bike>…

// syntax error: "super" not allowed here:
class Box<T super Bike> { ... }

T: Type Parameter Use

By leaving out the angle brackets, we can use a previously defined type parameter for the type of the following:

  • member variables
  • method
    • return values
    • parameters
    • local variables
class Box<T> {
  // member variable:
  T content;
}
// return value, parameter:
<T> T echo(T input) { 
  // local variable:
  T localVar = input;
  return localVar;
}

Besides, we can also use the type parameter as a type argument at all these locations. Don’t confuse this with type parameter definition, which uses the same angle bracket syntax:

// defining a type parameter:
class Box<T> {
  // using the type parameter from above 
  // as a type argument:
  List<T> content;
}

<?>: Wildcard

The unbounded <?> wildcard can be used as type argument, as well as its bounded counterparts:

List<?> anything;
List<? extends Bike> bikeProducer;
List<? super Bike> bikeConsumer;