1. Overview
JDK 5.0 introduced Java Generics with the aim of reducing bugs and adding an extra layer of abstraction over types.
This tutorial is a quick intro to Generics in Java, the goal behind them, and how they can improve the quality of our code.
2. The Need for Generics
Let’s imagine a scenario where we want to create a list in Java to store Integer.
We might try to write the following:
List list = new LinkedList();
list.add(new Integer(1));
Integer i = list.iterator().next();
Surprisingly, the compiler will complain about the last line. It doesn’t know what data type is returned.
The compiler will require an explicit casting:
Integer i = (Integer) list.iterator.next();
No contract could guarantee that the return type of the list is an Integer. The defined list could hold any object. We only know that we are retrieving a list by inspecting the context. When looking at types, it can only guarantee that it is an Object and therefore requires an explicit cast to ensure that the type is safe.
This cast can be annoying — we know that the data type in this list is an Integer. The cast is also cluttering our code. It can cause type-related runtime errors if a programmer makes a mistake with the explicit casting.
It would be much easier if programmers could express their intention to use specific types and the compiler ensured the correctness of such types. This is the core idea behind generics.
Let’s modify the first line of the previous code snippet:
List<Integer> list = new LinkedList<>();
By adding the diamond operator <> containing the type, we narrow the specialization of this list to only Integer type. In other words, we specify the type held inside the list. The compiler can enforce the type at compile time.
In small programs, this might seem like a trivial addition. But in larger programs, this can add significant robustness and makes the program easier to read.
3. Generic Methods
We write generic methods with a single method declaration, and we can call them with arguments of different types. The compiler will ensure the correctness of whichever type we use.
These are some properties of generic methods:
- Generic methods have a type parameter (the diamond operator enclosing the type) before the return type of the method declaration.
- Type parameters can be bounded (we explain bounds later in this article).
- Generic methods can have different type parameters separated by commas in the method signature.
- Method body for a generic method is just like a normal method.
Here’s an example of defining a generic method to convert an array to a list:
public <T> List<T> fromArrayToList(T[] a) {
return Arrays.stream(a).collect(Collectors.toList());
}
The
As mentioned, the method can deal with more than one generic type. Where this is the case, we must add all generic types to the method signature.
Here is how we would modify the above method to deal with type T and type G:
public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
return Arrays.stream(a)
.map(mapperFunction)
.collect(Collectors.toList());
}
We’re passing a function that converts an array with the elements of type T to list with elements of type G.
An example would be to convert Integer to its String representation:
@Test
public void givenArrayOfIntegers_thanListOfStringReturnedOK() {
Integer[] intArray = {1, 2, 3, 4, 5};
List<String> stringList
= Generics.fromArrayToList(intArray, Object::toString);
assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}
Note that Oracle recommendation is to use an uppercase letter to represent a generic type and to choose a more descriptive letter to represent formal types. In Java Collections, we use T for type, K for key and V for value.
3.1. Bounded Generics
Remember that type parameters can be bounded. Bounded means “restricted,” and we can restrict the types that a method accepts.
For example, we can specify that a method accepts a type and all its subclasses (upper bound) or a type and all its superclasses (lower bound).
To declare an upper-bounded type, we use the keyword extends after the type, followed by the upper bound that we want to use:
public <T extends Number> List<T> fromArrayToList(T[] a) {
...
}
We use the keyword extends here to mean that the type T extends the upper bound in case of a class or implements an upper bound in case of an interface.
3.2. Multiple Bounds
A type can also have multiple upper bounds:
<T extends Number & Comparable>
If one of the types that are extended by T is a class (e.g. Number), we have to put it first in the list of bounds. Otherwise, it will cause a compile-time error.
4. Using Wildcards With Generics
Wildcards are represented by the question mark ? in Java, and we use them to refer to an unknown type. Wildcards are particularly useful with generics and can be used as a parameter type.
But first, there is an important note to consider. We know that Object is the supertype of all Java classes. However, a collection of Object is not the supertype of any collection.
For example, a List