Generics
Generics are a facility of generic programming that were added to the Java programming language in 2004 within version J2SE 5.0. They were designed to extend Java’s type system to allow “a type or method to operate on objects of various types while providing compile-time type safety”.
The Java collections framework supports generics to specify the type of objects stored in a collection instance.
Motivation
The following block of Java code illustrates a problem that exists when not using generics. First, it declares an ArrayList
of type Object
. Then, it adds a String
to the ArrayList
. Finally, it attempts to retrieve the added String
and cast it to an Integer
— an error in logic, as it is not generally possible to cast an arbitrary string to an integer.
List v = new ArrayList();
v.add("test"); // A String that cannot be cast to an Integer
Integer i = (Integer)v.get(0); // Run time error
Although the code is compiled without error, it throws a runtime exception (java.lang.ClassCastException
) when executing the third line of code. This type of logic error can be detected during compile time by using generics and is the primary motivation for using them.
The above code fragment can be rewritten using generics as follows:
List<String> v = new ArrayList<String>();
v.add("test");
Integer i = (Integer)v.get(0); // (type error) compilation-time error
The type parameter String
within the angle brackets declares the ArrayList
to be constituted of String
(a descendant of the ArrayList
’s generic Object
constituents). With generics, it is no longer necessary to cast the third line to any particular type, because the result of v.get(0)
is defined as String
by the code generated by the compiler.
The logical flaw in the third line of this fragment will be detected as a compile-time error (with J2SE 5.0 or later) because the compiler will detect that v.get(0)
returns String
instead of Integer
.
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;
- 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 <T>
in the method signature implies that the method will be dealing with generic type T
. This is needed even if the method is returning void.
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.
Generic Classes
A generic class declaration looks like a non-generic class declaration, except that the class name is followed by a type parameter section.
As with generic methods, the type parameter section of a generic class can have one or more type parameters separated by commas. These classes are known as parameterized classes or parameterized types because they accept one or more parameters.
Following example illustrates how we can define a generic class:
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();
integerBox.add(new Integer(10));
stringBox.add(new String("Hello World"));
System.out.printf("Integer Value :%d\n\n", integerBox.get());
System.out.printf("String Value :%s\n", stringBox.get());
}
}
This will produce the following result:
Integer Value :10
String Value :Hello World
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.
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.
Links
Further reading
Java Generics Example Tutorial - Generic Method, Class, Interface
Language Features of Java Generics
Next Questions
What do you know about wildcard?