Generics (Java)

Generic types are commonly used in most modern programming languages. In the case of Java, they were added in Java 5 which was released in 2004. Even though it’s present in the language for many years, it still confuses some people on how to use them properly. Sometimes more experienced developers also lack full knowledge in this area, using only the simplest form of generic types.

My purpose here is to explain the concept in Java. I chose Java as it is still one of the most popular languages. I am also going to create a separate post to describe generics for Kotlin as this language is still gaining popularity and is often chosen instead of Java for new projects. But I think that understanding how it works in Java first is a good idea.

What is a generic type?

The most popular usage of generic types is to use them as class parameters. They enable the class to provide constructors, methods and fields where type-safety is preserved, i.e. it is guaranteed to use the same type in several places in the class even though the type is not known ahead of time.

A simple example of this is List type in Java, e.g. if the list is declared as List<String>, then it accepts and returns only String type in most of its methods – only Strings can be added to the list, retrieved and removed from it. If the list is declared as List<Boolean>, Strings cannot be added/retrieved anymore but only Booleans.
Thanks to generic types – the List class is declared once and it can accept any type ensuring no other types can be used there. Without the generic types, the List class would have to accept and return Object type which would be inconvenient as retrieving String or Boolean from the list would require type casting.

Generic types can also be applied to interfaces or limited to single methods/functions so I am going to present all the possibilities below.

What does the generic type look like?

Generic class

Class declaration

An example of such a generic class in Java is built-in LinkedList class. Let me present a small fragment of the class below in a simplified form (I removed some code not relevant to this topic) to present the concept.

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E> {
    Node<E> first;

    public LinkedList(Collection<? extends E> c) { // some code here }

    public E getFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return f.item;
    }

    public void add(E e) { // some code here }
}

The <E> is a declaration of a generic type which is named E in this class. Nearly any name can be chosen for the generic type – the only requirement is to use the same name everywhere in the scope of the class.

The generic type E is applied in this case to:

  • the class itself which declares the E generic type,
  • AbstractSequentialList class which is also a generic class extended by this class – as LinkedList extends AbstractSequentialList, it must inform AbstractSequentialList class about the expected data type,
  • List interface which is a generic interface implemented by this class so just as above – this class must inform List interface about the expected type of data,
  • Node<E> first field which again indicates usage of another generic class Node which must be informed about data type,
  • Collection<? extends E> type accepted in a constructor – I am explaining this <? extends E> in the further part of this article but for now, just be aware that the generic type declared for the class can be applied also to the constructor’s parameter,
  • E getFirst() method which uses the generic type as its return type. Methods can also use the generic type for their parameters and somewhere inside the implementation.

Just to summarize – the generic type can be used anywhere in the class – whenever a type is needed, it can be used as a type.

Class usage
LinkedList<Integer> list = new LinkedList<Integer>();
list.add(1);
Integer firstNumber = list.getFirst();

So in the first line, an object of the class is declared along with its generic type – Integer in this case. The add method is used to add an element to the list. The method accepts only Integer due to the declared generic type. An attempt to pass anything else than Integer to the method results in a compilation error so it is impossible.

Similarly, getFirst is used to retrieve an element from the List and it is always an Integer – thanks to generic type and inability to add anything else than Integer to the list – it is safe to assume that getFirst method always return Integer so there is no need for type casting.

Thanks to some improvements added to newer Java versions, it is possible to shorten the code above a bit:

LinkedList<Integer> list = new LinkedList<>(); // diamond operator
var list2 = new LinkedList<Integer>(); // type inference

The first line is equivalent to the first line in the previous example. When a type is already known, it can be skipped and the compiler will infer it automatically – in this case, the generic type is provided when declaring a field’s type so the compiler knows what is the generic type and it can be inferred. The operator <> is called a diamond operator and means that the compiler should infer the type.

The second line is also equivalent to the line above but in this case, a var keyword is used which is yet another kind of type inference in Java. In this case, the type is skipped on the left side so the full type must be declared on the right side. More information about var keyword can be found here: Java 10 Local Variable Type Inference.

What happens when a class uses generic types but the type is not provided?

LinkedList list = new LinkedList();
var list2 = new LinkedList();

In cases like this – Java uses Object type as the generic type. The compiler reports warnings about “raw usage of parametrized class” which just means that no generic type is provided to the generic class.

Generic interface

Interface declaration

As mentioned earlier, interfaces can also be parametrized by generic types. The LinkedList class implements two generic interfaces, one of them is the List interface which is visible in the example above.

Let me present a simplified version of the List interface in Java.

public interface List<E> extends Collection<E> {
    boolean add(E e);
    List<E> subList(int fromIndex, int toIndex);

    default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }
}

In the case of interfaces, the generic type can be applied to:

  • other interfaces extended by the interface – like Collection<E> in this case,
  • parameters in declared methods – like the add method in this case,
  • return types in declared methods – like the subList method in this case,
  • anywhere in default methods – in return types, parameters but also in the implementation of such methods like in the replaceAll method in this example.
Interface usage

The generic interface can be used in the same places where the interface is typically used, i.e. as a type of parameters, fields, variables and in class or interface declarations where the interface is referred. An example of how to implement or extend a generic interface is presented in two examples above – in the LinkedList example which implements the List interface and in the List example which extends Collection generic interface.

This is an example of how to use a generic interface as a type for a local variable:

List<Integer> list = new LinkedList<>();
list.add(1);
list.add(2);
List<Integer> subList = list.subList(1, 2);

Generic method

Method declaration

Single methods in classes or interfaces can also be generic even when the whole class or interface is not generic. An example of this is the built-in Java Collections class which is presented below in a simplified form with one of its methods.

public class Collections {
    public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<T> s) {
        return new UnmodifiableSortedSet<>(s);
    }
}

In this case, <T> placed after the static keyword declares the generic type T which is available only in the scope of this method. For a non-static method it would be declared right after the visibility modifier (here it would be after public).

The T type can be applied to anything in the method, i.e. to:

  • return typeSortedSet<T> in this case,
  • parameter – also SortedSet<T> in this case,
  • anywhere in the body of the method – here it is used indirectly when returning an instance of UnmodifiableSortedSet type where the generic type T is provided using a diamond operator so its equivalent would be return new UnmodifiableSortedSet<T>(s);
Method usage

This is an example of how to use a generic method:

SortedSet<Integer> set1 = new TreeSet<>();
set1.add(1);
SortedSet<Integer> set2 = Collections.unmodifiableSortedSet(set1);

So there is no need to do anything special when using a generic method.
In particular, there is usually no need to declare the generic type when using the method because it infers the type automatically from the code, e.g. from the passed parameters or there the method may be void but it simply wants to guarantee that the same type is used in several places in method’s body.

There are some cases though when the type cannot be inferred and must be passed explicitly but it is described below in the separate section.

Generic type name collision

The generic type declared for a method takes precedence over the generic type declared for the class or interface. Consider the following class:

class GenericClass<T> {
  public T method1(T param) { return param; }
  public <T> T method2(T param) { return param; }
}

in the case of method2 the type T refers to the type declared only for the method. It hides the type T declare for the class making it possible to use two different types in both places. The code below compiles successfully and works correctly.

GenericClass<Integer> obj = new GenericClass<>();
obj.method1(1);
obj.method2("NotAnInteger");

Please note that while it is possible, it is not a good practice to hide the generic type declared for the class and would be much better to use another non-colliding name for the generic type of method2.

Static method

Let’s get back to the List interface but this time let’s check some of the other methods provided by the interface.

public interface List<E> extends Collection<E> {
    static <E> List<E> of(E e1, E e2) {
        return new ImmutableCollections.List12<>(e1, e2);
    }

    static <E> List<E> copyOf(Collection<? extends E> coll) {
        return ImmutableCollections.listCopy(coll);
    }
}

Even though the first impression may be that there is again a generic type name collision, in fact, there is no such a collision in this case.

It results from the way the generic types are used. The types are declared for an interface or a class in the context of their instances – they can be used when instances of such classes are created (or instances of classes implementing such interfaces).

When a method is static, it is not bound to any particular object and it may be used without any object. Hence, in such a case there is no declaration of generic type. There is no possibility to declare the generic type like this:

List<Integer>.of(1, 2);

So in the case of static methods they can have generic types used only in their own scopes like for the two methods presented above: of and copyOf methods in the List interface.

The usage of such a static method is already presented above for Collections.unmodifiableSortedSet method.

Passing type to generic method

There is one more case which needs to be explained. Consider the following simplified code (it is delivered by Guava library):

public abstract class ImmutableList<E> {
  public static <E> ImmutableList.Builder<E> builder() {
    return new ImmutableList.Builder();
  }

  public static final class Builder<E> {

    public ImmutableList.Builder<E> add(E element) { // some code }
  }
}

In this case, the static method builder is parametrized by a generic type E and returns an instance of a generic class ImmutableList.Builder which has its own non-static method add using the generic type provided to its class.

The generic type must be provided explicitly to this builder static method as it has no way to infer the type by itself.

ImmutableList.<String>builder()
  .add("someString") // only String can be passed here
  .build();

In this case, an attempt to pass, e.g. Integer to add method of the builder would result in the compilation error. Without the type passed explicitly, Java assumes the type to be Object so the following code is valid:

ImmutableList.builder()
  .add("someString")
  .add(1)
  .build();

Covariance and contravariance – how to make generic types even more generic

While generic typing presented above is already a powerful tool, there are two more things in generics which extend their usability – covariance and contravariance. The terms are more general ones as they do not apply only to generic types but they will be explained here in the area of generics.

Covariance and contravariance allow us to define a set of types bounded by some type rather than limiting a generic to a single type. They also mean something more so check the examples and definitions below for both of them.

Covariance

Example

Consider the following lists with locks.

List<Lock> list1 = List.of(new ReentrantLock());
List<ReentrantLock> list2 = List.of(new ReentrantLock());

Both of them contain a single item which is an instance of ReentraceLock class implementing Lock interface. Both declarations are valid then as the first list may include objects of all classes implementing Lock interface and the second list is restricted to ReentrantLock type only. As objects of this type are passed to both lists, there is no reason to complain at this point.

Now, imagine a method which should invoke lock method (declared in Lock interface) for each lock included in the list. Basically, it can look like this:

void lockAll(List<Lock> locks) {
  locks.forEach(Lock::lock);
}

However, even though each object in list2 has lock method from Lock interface, the list cannot be passed to this method – such an attempt results in a compilation error. Only list1 can be passed here. So it is illegal to assign list2 to list1 even though each object from list2 can be assigned to Lock typed variable.

lockAll(list1); // works well
lockAll(list2); // compilation error
list1 = list2; // compilation error
Lock lock = list2.get(0); // works well

To make it work properly, Java must be informed that List<ReentrantLock> is a subtype of List<Lock>. In fact, it is not true but there is a way to do something similar.

Covariance can be used to solve this issue. Simply use List<? extends Lock> instead of List<Lock>. It means that the list may contain anything of the Lock type (Lock and all its implementations or subclasses in case some class type would be used instead of Lock type).

The method can be changed to:

void lockAll(List<? extends Lock> locks) {
  locks.forEach(Lock::lock);
}

and after the change, it is okay to pass list2 to the method. Similarly, when changing the type of list1 to List<? extends Lock>, it is valid to assign list2 to the variable.

lockAll(list1); // works well
lockAll(list2); // works well

List<? extends Lock> newList;
newList = list1; // works well
newList = list2; // works well
Definition

Now it should be easy to understand what is covariance. Type C parametrized by generic type T is covariant if the type C<A> is a subtype of C<B> when A is a subtype of B.

For the example presented above: ReentrantLock is a subtype of Lock so List<ReentrantLock> is a subtype of List<? extends Lock>. Hence, List<ReentrantLock> can be used in all places where List<? extends Lock> type is required.

The ? extends Lock made the list covariant, List<Lock> is not covariant because List<ReentrantLock> is not a subtype of List<Lock>.

Limitations

Covariance means it is safe to read data but not to write the data. What does it mean? Let’s consider the following code:

List<? extends Lock> list = List.of(new ReentrantLock()); // works well
Lock lock = list.get(0); // works well
list.add(new ReentrantLock()); // compilation error

Covariance makes it possible to retrieve data from the list but makes it impossible to add anything to the list because it could lead to an illegal state.

class SomeLock implements Lock { ... }

List<? extends Lock> list;
List<SomeLock> list2 = List.of(new SomeLock());
list = list2; // works well
Lock lock = list.get(0); // works well
list.add(new ReentrantLock()); // compilation error

SomeLock is a class implementing Lock interface but it is not a subtype of ReentrantLock.
First, a covariant list accepting any Lock is declared.
Then a list of the type List<SomeLock> is assigned to List<? extends Lock> so the list becomes in fact a list of SomeLock.

Clearly, it is okay to get any element from the list and assign it to Lock type because every SomeLock is a subtype of Lock.
But it would not be okay to add ReentrantLock to the list because its value is a list of SomeLock and ReentrantLock is not a subtype of SomeLock.

This is why covariance means that the generic type T may be used as a return type but not as a parameter type.

Contravariance

Example

Consider a method which should copy all locks from one list to the other one.

void copyLocks(List<? extends Lock> from, List<Lock> to) {
  to.addAll(from);
}

The method works correctly and accepts any list containing objects of Lock type as the source list so it is easy to pass source lists like List<ReentrantLock> or List<SomeLock> (refer to its definition in Covariance section above).

However, when passing the destination list the only possibility is to pass List<Lock> type.
Imagine the the Lock interface is a subtype of BaseLock.

interface BaseLock { ... }
interface Lock implements BaseLock { ... }

List<ReentrantLock> list1 = List.of(new ReentrantLock());
List<Lock> list2 = List.of();
List<BaseLock> list3 = List.of();

copyLocks(list1, list2); // works well
copyLocks(list1, list3); // compilation error

It is impossible to pass List<BaseLock> to copyLocks as the destination list even though it would be safe for this particular method to add all elements of Lock type to list of BaseLock as each Lock is a subtype of BaseLock.

Contravariance can be used to fix the issue by changing the declaration of the destination list to List<? super Lock> to.

void copyLocks(List<? extends Lock> from, List<? super Lock> to) {
  to.addAll(from);
}

List<ReentrantLock> list1 = List.of(new ReentrantLock());
List<Lock> list2 = List.of();
List<BaseLock> list3 = List.of();

copyLocks(list1, list2); // works well
copyLocks(list1, list3); // works well

The change means that any list can be passed as the destination list to the method as long as the list is parametrized by type being a supertype of Lock so in this case it is acceptable to pass here List<Lock>, List<BaseLock> and List<Object> types because only Lock, BaseLock and Object are subtypes of Lock. So it is illegal to pass e.g. List<ReentrantLock> because ReentrantLock is not a supertype of Lock but its subtype.

An important thing to note here is that List<BaseLock> is a subtype of List<? super Lock> but BaseLock is a supertype of Lock. So it is an inversion of what happened for Covariance.

List<? super Lock> contravariantList = List.of();
List<? extends BaseLock> covariantList = List.of();
List<Lock> list1 = List.of();
List<BaseLock> list2 = List.of();
contravariantList = list2; // works well
covariantList  = list1; // works well
Definition

Now it should be easy to understand what is contravariant. Type C parametrized by generic type T is contravariant if the type C<B> is a subtype of C<A> when A is a subtype of B. In other words – C<A> is a supertype of C<B>.

Comparing to covariant – for the same relation between A and B, the relationship between C<A> and C<B> is inversed.

For example, ReentrantLock is a subtype of Lock so List<Lock> is a subtype of List<? super ReentrantLock>. Hence, List<Lock> can be used in all places where List<? super ReentrantLock> type is required.

The ? super ReentrantLock made the list contravariant, List<ReentrantLock> is not contravariant because List<Lock> is not a subtype of List<ReentrantLock>.

Limitations

Contravariance means it is safe to write data but not to read the data so it is inversed compared to covariance. Let’s consider the following code:

List<? super Lock> list = List.of(new ReentrantLock()); // works well
list.add(new ReentrantLock()); // works well
Lock lock = list.get(0); // compilation error

list.get(0) can be assigned only to a variable of Object type because there is no information about the type even though we know there is ReentrantLock which is a subtype of Lock.
Contravariance makes it possible to write data to the list but makes it impossible to have any information about the type of data retrieved from the list. Why? It is similar to what happened in the case of covariance.

List<? super Lock> list;
List<Object> list2 = List.of("someString", 5, true);
list = list2; // works well
list2.get(0); // returns String, not a Lock
list2.get(1); // returns Integer, not a Lock
list2.get(2); // returns Boolean, not a Lock

Now it should be clear. As List<Object> can be used in place of any contravariant list, elements of any type can be placed in the list. Hence, the only valid return type in contravariant classes, interfaces and methods is Object.

This is why contravariance means that the generic type T can be used as an input type (a type of parameter) but not as a return type – contravariant class may accept any data in parameters but there is no knowledge of what is really returned.

Invariance

When generic types are used in a way described in What does the generic type look like section, i.e. without extends or super keywords – they are invariant.

Invariance is a situation when A is a subtype of B but C<A> is not a subtype of C<B> and C<B> is not a subtype of C<A>. For example, List<Lock> cannot be assigned to List<ReentrantLock> and vice-versa.

Summary

The definitions for invariance, covariance and contravariance are unclear to many people. Similarly, the limitations for data read and write operations in the case of covariance and contravariance – it is especially confusing why they are present.

However, it is really important to understand the concepts and the reasons behind the limitations to reach full fluency when using generics. This way, it is possible to use their full power.

I hope that this article makes it clear and that there will be no more confusion around the topic. However, if you find anything unclear and you have still some issues with understanding after reading this article – let me know so I can improve my explanations.

In the next parts, I am going to explain Type Erasure and show how the Generics are implemented in Kotlin language.

Leave a Reply

Your email address will not be published. Required fields are marked *