Generics in Java

Generics were introduced with Java 6 and quickly become indispensable tool of every Java programmer. In this blog post I gathered the most important facts about generics in Java. After reading this post you you should be able to comfortable use generics in your code.

Generic classes

We can declare generic Pair class using syntax:

public class Pair<E1,E2> {
    private final E1 first;
    private final E2 second;

    public Pair(E1 first, E2 second) {
        this.first = first;
        this.second = second;
    }

    public E1 getFirst()  { return first;  }
    public E2 getSecond() { return second; }
}

Then we can use Pair as follows:

// java 6:
Pair<String,Integer> p1 = new Pair<String,Integer>("foo", 10);
  
String first = p1.getFirst();
int second = p1.getSecond();
  
// java 7+ - using diamond operator <>
Pair<String,Integer> p2 = new Pair<>("foo", 10);

In Java 7 and later we can let compiler infer values of generic parameters in new expression by using diamond operator (<>).

Bounds

We can reduce possible values of generic parameters by using bounds. For example to reduce values of parameter T to types that implement Serializable interface we can write:

public class SerializableList<T extends Serializable> { }

When using bounds we are not limited to single type. For example we may require that types allowed for T must extend MyBaseClass and implement Serializable and Cloneable interfaces:

public class SerializableList2<
   T extends MyBaseClass & Serializable & Cloneable> { }

When we try to use SerializableList with types that doesn’t conform to our bounds we will get compile-time error:

// ok
SerializableList<Integer> ints = new SerializableList<>();

// error: type argument java.lang.Object is not within bounds of type-variable T
SerializableList<Object> objs = new SerializableList<Object>();
Type erasure

In Java generics are implemented via type erasure, this means that generics exists only in Java source code and not in JVM bytecode. When Java compiler translates generic classes to bytecode it substitutes generic parameters in class body with Object or if generic parameter has bounds with value of the first bound. For example our Pair class would be translated by compiler into:

public class Pair {
    private final Object first;
    private final Object second;

    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst()  { return first;  }
    public Object getSecond() { return second; }
}

Compiler also inserts necessary casts, and converts between primitives and wrappers (e.g. between int and Integer) - a process called (un)boxing. Continuing our example:

pair = new Pair<String,Integer>("foo", 10);

String first = pair.getFirst();
int second = pair.getSecond();

is translated by compiler into:

pair = new Pair("foo", 10);

String first = (String)pair.getFirst();
int second = ((Integer)pair.getSecond()).intValue();

Generics were implemented via type erasure to preserve binary compatibility with pre Java 6 code (binary compatibility means that your old code will work with generic types out of the box - you don’t need to change or recompile your legacy libraries).

Shortcomings of type erasure

Type erasure is not the best way to implement generics, and IMHO Java should take a different approach (e.g. reification). But Java didn’t and we are stuck with “type erasure” generics. Below is a list of Java generics shortcomings:

  • Primitive types like int cannot be used with generics. We may use generics with wrapper types e.g. Integer but this will incur performance penalty caused by casts and boxing/unboxing operations.
  • We cannot use generic parameters in declarations of static class members. Static class members are shared between all instances of generic class regardless of generic parameters values. To stress this fact Java allows to access static members only via class name without generic parameters:
class Example<T> {
    // error: non-static type variable T cannot be referenced from a static context
    // private static T last;

    private static int counter = 0;
    public static void printCounter() {
        System.out.println("counter: " + counter);
    }

    public Example() { counter++; }
}

new Example<Integer>();
new Example<String>();

// error: not a statement
// Example<Integer>.printCounter();

// prints counter: 2
Example.printCounter();
  • Sometimes Java compiler must create synthetic methods (in this case called bridge methods) to make overriding work with generic types. For example:
interface TestInterface<T> {
    void consume(T value);
}

class TestClass implements TestInterface<Integer> {
    @Override
    public void consume(Integer value) {
        System.out.println(value);
    }
}

after type erasure becomes:

interface TestInterface {
	void consume(Object value);
}

class TestClass implements TestInterface {
    public void consume(Integer value) {
        System.out.println(value);
    }
}

and we can see that TestClass no longer overrides consume method from TestInterface. To solve this problem compiler adds following method to TestClass:

class TestClass implements TestInterface {
   // bridge method added by compiler:
   @Override public void consume(Object value) {
	 this.consume((Integer)value);
   }

   public void consume(Integer value) { ... }
}
  • Generics don’t work well with overloading, for example following overloads are forbidden:
public void check(List<Integer> ints) { }
public void check(List<String> strings) { }

because after type erasure both methods have exactly the same signature

public void check(List list) { }

Similarly we cannot implement the same interface twice with different generic parameters. Again type erasure is our culprit:

public interface Test<T> { }
// error: Test cannot be inherited with 
//	  different arguments: <Integer> and <String>
public class TestImpl 
	implements Test<Integer>, Test<String> { }
Raw types and unchecked warnings

To maintain backward compatibility Java allows us to use generic types without specifying generic parameters. Such types are called raw types, for example:

// preferred usage of generics:
List<Integer> typedList = new ArrayList<Integer>();

// raw type:
List rawList = new ArrayList<Object>();
rawList.add("foo"); // unchecked warning

Raw types can be treated like generic types after type erasure. Raw types should only be used to interact with legacy code.

When working with raw types compiler may generate an unchecked warning:

warning: unchecked call to add(E) as a member of the raw type java.util.List

This warning means that compiler is not sure if we used generic type correctly and in case that we didn’t we should expect ClassCastException at runtime. For example:

public static void main(String[] args) {
  List<Integer> ints = new ArrayList<Integer>();

  List rawList = ints;
  legacyCode(rawList);

  int x = ints.get(0); // ClassCastException
}

public static void legacyCode(List list) {
  list.add("foo"); // unchecked warning
}

The problem here is that the client of legacyCode expected that legacyCode will add integer to provided list. A simple solution is to use List<Object> instead of List<Integer> if legacyCode may add different types to list. Notice also that line which generated unchecked warning didn’t throw any exception, exception was thrown later when the client wanted to access list element.

We may suppress unchecked warning at the method or class level by using @SuppressWarnings("unchecked") annotation.

Generic methods

We can declare generic method as follows:

public static <E1,E2> Pair<E1,E2> pair(E1 first, E2 second) {
   return new Pair<E1,E2>(first, second);
}

Generic methods are invoked like ordinary methods:

Pair<String,Integer> p1 = pair("foo", 10);

In most cases compiler will be able to infer proper values of generic parameters. When it won’t we can override compiler by explicitly specifying generic parameters values:

ClassName.<String,Number>staticMethod(arg1, arg2);
// or
this.<String,Integer>instanceMethod(arg1, arg2);

// syntax error:
// <String,Integer>method(arg1, arg2);

Wildcards

Let’s consider method that copies elements from one list to another:

public static <T> void copy(List<T> dest, List<T> src) {
   for (T element: src) {
	  dest.add(element);
   }
}

It works perfectly with lists of integers:

List<Integer> src = Arrays.asList(1,2,3);
List<Integer> dest = new ArrayList<>();
copy(dest, src);

But fails when we want to copy integers to list of numbers:

List<Integer> src = Arrays.asList(1,2,3);
List<Number> nums = new ArrayList<>();
// error: method cannot be applied to given types
copy(nums, src);

We may fix method by adding second generic parameter:

public static <D,S extends D>
void copy(List<D> dest, List<S> src) {
   for (S element: src) {
	  dest.add(element);
   }
}

This is so common situation that Java introduces a shortcut:

public static <T> void copy(List<T> dest, List<? extends T> src) {
  for (T element: src) {
	  dest.add(element);
  }
}

Type List<? extends T> means that this is a list of elements that extends or implements type T.

Wildcards allows us to reduce number of required generic parameters and made method declarations more clear, for example:

public static <T> boolean isNullOrEmpty(Collection<T> coll) {
  return coll == null || coll.isEmpty();
}

public static boolean isNullOrEmptyWildcards(Collection<?> coll) {
  return coll == null || coll.isEmpty();
}

Here Collections<?> means collection of elements of some certain type e.g. this may be Collection<Object> or Collection<MyClass>.

super bound

super bound may be used only with wildcards. super bound restricts values of wildcard to given class and all of its superclasses, for example method:

void process(List<? super Integer> list) { }

can only be used with List<Integer>, List<Number> and List<Object>. Calling method with List<String> results in compile time error.

While extends bound is useful when we want to get values from generic type instance, super bound is needed when we want to pass values to generic type instance. For example:

static <T> void produceConsume(
	  Producer<? extends T> producer,
	  Consumer<? super T> consumer)
{
  for(;;) {
	  T value = producer.produce();
	  consumer.consume(value);
  }
}

Here Producer may produce type T or more derived type, and Consumer may consume type T or more general type e.g. Object. Thanks to wildcards we may use produceConsume with Producer<Integer> and Consumer<Object>.

NOTE: Java compiler tries to infer the most specific type for generic parameters. In call to produceConsume with Producer<Integer> and Consumer<Object> Integer will be used as T parameter value.

Wildcard capture

Let’s say that we want to create a method that swaps elements of the list, we may write:

public static void swap(List<?> list, int i1, int i2) {
   // doesn't compile
   ? tmp = list.get(i1);
   list.set(i1, list.get(i2));
   list.set(i2, tmp);
}

Unfortunately above code doesn’t compile. We may either introduce generic parameter to method signature or create a helper method with generic parameter that will “capture” wildcard value:

public static void swap(List<?> list, int i1, int i2) {
   swapImpl(list, i1, i2);
}
private static <T> void swapImpl(List<T> list, int i1, int i2) {
   T tmp = list.get(i1);
   list.set(i1, list.get(i2));
   list.set(i2, tmp);
}

Introducing generic parameter is always better solution than using wildcard capture. I only mention above technique because it is often used in Java Collection Framework.

Covariance and contravariance

With Java arrays we may write:

String[] strings = { "foo", "bar" };
Object[] objects = strings;

We say that Java arrays are covariant.

Generics in Java are invariant this means that below code doesn’t compile:

List<String> strings = Arrays.asList("foo", "bar");
// error: incompatible types
List<Object> objects = strings;

We must tread List<String> and List<Object> as two distinct types.

Still we may use wildcards to refer to either List<String> or List<Object>:

List<String> strings = Arrays.asList("foo", "bar");
List<Object> objects = Arrays.asList(true, 1, "foo");

List<?> list = strings;
list = objects;

List<?> should be treated as superclass of any List<T>, because it represents list of objects of some certain type.

We can’t do much with List<?>, we can only get Objects from it, add nulls and ask for size (operations allowed for any list):

List<?> list = strings;

list.add(null);
Object value = list.get(0);
list.size();

Operations on List<?> are limited because we don’t know what types list contains. We may limit range of possible types with bounds thus gaining more functionality:

List<? extends Number> numbers = Arrays.asList(1.2, 3.5);
Number num = numbers.get(0);

Now compiler knows that list elements are at least numbers so we may assign result of get() to variable of type Number. Still we are not able to put anything beyond null into list, because we don’t know if this is a list of doubles or a list of integers.

When we want to add elements to list we should use super bound:

List<? super Number> numbers =
   new ArrayList<Object>(Arrays.asList("foo", true));

numbers.add(3);
numbers.add(3.2);

Now compiler knows that list holds numbers or elements more general than numbers e.g. objects, so adding number to list is safe.

How to use generic types with instanceof and class

Because of type erasure types List<Object> and List<Integer> are indistinguishable to JVM. To check if value is instance of List we may write:

Object value = new ArrayList<Integer>();

if (value instanceof List<?>) {
   // do something
}

Similarly types List<Object> and List<Integer> are represented by the same class token:

List<Integer> integers = new ArrayList<Integer>();
List<Object> objects = new ArrayList<Object>();

Class<? extends List> integersClazz = integers.getClass();
Class<? extends List> objectsClazz = objects.getClass();
Class<ArrayList> arrayListClazz = ArrayList.class;

// true
System.out.println(integersClazz.equals(objectsClazz));
// true
System.out.println(integersClazz.equals(arrayListClazz));

Notice that in instance test we should use type with wildcard (List<?>) but to get class token we should use raw type (ArrayList.class).

Generics and arrays

Let’s consider this innocent looking code:

public static <T> T[] toArray(T v1) {
  T[] array = (T[]) new Object[1]; // unchecked warning
  array[0] = v1;
  return array;
}

Calling this method results in ClassCastException:

String[] s = toArray("foo"); // class cast exception

Because we cannot assign Object[] instance to String[] variable. The source of the trouble is type erasure again. Because value of parameter T is not accessible at runtime we don’t know what array we should create - should it be array of objects or maybe array of strings. We may fix this method by passing additional parameter that will represent required type of array elements:

public static <T> T[] toArray(T v1, Class<T> type) {
  T[] array = (T[]) Array.newInstance(type, 1);
  array[0] = v1;
  return array;
}

String[] s = toArray("foo", String.class);

Now method works as expected but is cumbersome to use.

Another problem with arrays and generics is that we cannot create array with generic elements - type erasure is culprit again:

// error: generic array creation
// List<Integer>[] lists = new List<Integer>[3];

List<Integer>[] lists = (List<Integer>[]) new List[3];

To create array of generic types we must use raw type and cast.

To sum up: you should avoid mixing arrays and generics.

Generics and varargs

Java varargs methods are implemented using arrays, when we try to use varargs with generics:

public static <T> List<T> concat(List<? extends T>... lists) {
   List<T> concatenated = new ArrayList<T>();

   for (List<? extends T> l: lists) {
	  concatenated.addAll(l);
   }

   return concatenated;
}

compiler issues a warning:

warning: unchecked generic array creation for varargs parameter

Generic varargs suffer from the same problems as generic arrays. If we are sure that our code is safe, we may use @SafeVarargs annotation to suppress this warning.

Additional resources

If you want to know more about generics check resources below:

marcin-chwedczuk

A Programmer, A Geek, A Human