Learning Generics by Understanding signature of Collections.copy() method

Ankur Saxena
Xebia Engineering Blog
4 min readNov 13, 2019
Photo by Evan Dennis on Unsplash

Most Java developers would have used Collections class at least once in their lifetime. One of the methods in the Collections class is the copy() method. The copy() method takes two arguments which are both Lists and copies all the elements of one to the other.

If we look at the copy() method in java.util.Collections class, it looks like this.

public static <T> void copy(List<? super T> dest, List<? extends T> src)

I recently switched to Java and I had a basic understanding of generics. The generics feature was added in JDK 1.5. Generics gives the ability to parameterize a type or method and use the code in a generic way. Generics serves two main purposes:

  • Compile-time type safety, which means the compiler guarantees that if correct types are used in correct places then there should not be any ClassCastException at runtime. This exception is thrown to indicate that the code has attempted to cast an object to a subclass of which it is not an instance.
  • Eliminating explicit type cast.

Upon looking at the copy() method, the first thing that came into my mind was what are <T>, <? super T> and <? extends T>. Let’s understand them one by one.

We can see that by using <T> before the return type of the method, we are making our method generic. This means I can use the same code irrespective of whether I want to copy List of String or List of Integer. I don’t have to write new methods to handle new types. The best part is that I don’t have to use Object type and use type casting all around in my code.

But what about <? super T> and <? extends T>.

Before getting into the theory of the copy() method, we must understand wildcards, upper bounding, and lower bounding. In generics ? (called as an unbounded wildcard) is used to accept just any type of object. However, we can restrict this behavior by applying a Lower-Bound or Upper-Bound to wildcard ?.

Upper-bound is when we specify (? extends Field) means argument can be any Field or subclass of Field.

Lower-bound is when we specify (? super Field) means argument can be any Field or superclass of Field.

The generic method copy() uses two wildcards as its parameters. The parameter dst must be a List whose element type is a supertype of T, and the parameter src must be a List whose element type is a subtype of T. Let us look at some other possible signatures of the copy() method and try to understand it more clearly.

Here I have three possible signatures of the copy() method:

public static <T> void copy(List<T> dst, List<? extends T> src)
public static <T> void copy(List<? super T> dst, List<T> src)
public static <T> void copy(List<? super T> dst, List<? extends T> src)

Let’s consider each of the signatures one by one.

public static <T> void copy(List<T> dst, List<? extends T> src)

If we invoke this method without an explicit type parameter, the type parameter will be inferred as Object, as we are passing List<Object> as the first argument. And then List<? extends Object> can accept an Integer. However, if we invoke with explicit type argument Number, although we can pass a List<Integer> to List<? extends Number>, the same is not true for List<Object> and List<Number>, as generics are invariant.

public static <T> void copy(List<? super T> dst, List<T> src)

Again for the implicit type parameter, T will be inferred as Integer, as we are passing List<Integer> as 2nd argument to List<T>. And then List<Object> is a valid substitute for List<? super Integer>. If we invoke the method with explicit type argument Number, we can pass List<Object> as 1st argument, but we can’t pass List<Integer> as 2nd argument. For the same reason explained above.

public static <T> void copy(List<? super T> dst, List<? extends T> src)

Now, this method signature will work for any type. For whatever type parameter being inferred, dst is the consumer of T instance, whereas src is the producer of T instance. E.g., if we invoke with explicit type argument Number, then List<Object> is capture convertible to List<? super Number>, similarly, List<Integer> is capture convertible to List<? extends Number>. So, both arguments are a valid substitution.

So, in all the cases above, the compiler can correctly infer the type parameters if we don’t provide one explicitly. But we must use the signature used in the original copy() method.

The reasons being:

  1. dst is the consumer of T instance, so it should use lower bounds, and
  2. src is the producer of T instance, so it should use upper bounds.

--

--