Skip to content

Generics

Generics are among the most complex language constructs in Java. Type testing of the compiler and the runtime environment An important feature of Java is that the compiler checks the types and thus knows which properties are present and which are not.

In Java, there are two instances that check the types, and these are differently smart. We have the JVM with absolute type intelligence, which runs our application and is the last instance to check that we are not assigning an object to the wrong type. Then we have the compiler, which checks well, but is sometimes a bit too gullible and follows the developer. If the developer makes mistakes, this error can cause the JVM to fail and lead to an exception. Everything has to do with the explicit type conversion. An initially uncomplicated example:

Object o = "String";
String s = (String) o;
The object o for a string is sold to the compiler via the explicit typecast. This is okay, because o actually references a string object. It becomes problematic if the type cannot be brought to String, but we instruct the compiler to perform a type conversion:
Object o = Integer.valueOf( 42 ); // or with autoboxing: Object o = 42;
String s = (String) o;
The compiler accepts the type conversion, and no error occurs at compile time. However, it is clear that this adaptation cannot be performed by the JVM - therefore a ClassCastException follows at runtime, since an integer cannot be converted to string. Generics is now about giving the compiler more information about the types and avoiding ClassCastException errors.

Generics extend the type system of Java to provide a type or method that can operate over objects of various types while providing compile-time type safety. In particular, the Java collections framework supports generics to specify the type of objects stored in a collection instance.

Creating a Generic Class

Generics enable classes, interfaces, and methods to take other classes and interfaces as type parameters. This example uses generic class Param to take a single type parameter T, delimited by angle brackets (<>):

public class Param<T> {    
  private T value;
  public T getValue() {
    return value;
  }
  public void setValue(T value) {
    this.value = value;    
  }
}

To instantiate this class, provide a type argument in place of T. For example, Integer:

Param<Integer> integerParam = new Param<Integer>();
The type argument can be any reference type, including arrays and other generic types:
Param<String[]> stringArrayParam; 
Param<int[][]> int2dArrayParam; 
Param<Param<Object>> objectNestedParam;

Extending a generic class

The following example shows the implementation of an abstract generic class:

public abstract class AbstractParam<T> {    
  private T value;
  public T getValue() {        
    return value;    
  }
  public void setValue(T value) {      
    this.value = value;
  }
}

AbstractParam is an abstract class declared with a type parameter of T. When extending this class, that type parameter can be replaced by a type argument written inside <>, or the type parameter can remain unchanged. In the first and second examples below, String and Integer replace the type parameter. In the third example, the type parameter remains unchanged. The fourth example doesn't use generics at all, so it's similar to if the class had an Object parameter.

public class Email extends AbstractParam<String> {
  // ... 
}
public class Age extends AbstractParam<Integer> {
  // ... 
}
public class Height<T> extends AbstractParam<T> {
  // ... 
}
public class ObjectParam extends AbstractParam {
  // ... 
}

The following is the usage:

Email email = new Email(); 
email.setValue("test@example.com"); 
String retrievedEmail = email.getValue();
Age age = new Age(); 
age.setValue(25); 
Integer retrievedAge = age.getValue(); 
int autounboxedAge = age.getValue();
Height<Integer> heightInInt = new Height<>(); 
heightInInt.setValue(125);
Height<Float> heightInFloat = new Height<>(); 
heightInFloat.setValue(120.3f); 

Multiple type parameters

We can use more than one type parameter in a generic class or interface. Multiple type parameters can be used in a class or interface by placing a comma-separated list of types between the angle brackets. Example:

public class MultiGenericParam<T, S> {    
  private T firstParam;    
  private S secondParam;       
  public MultiGenericParam(T firstParam, S secondParam) {        
    this.firstParam = firstParam;        
    this.secondParam = secondParam;    
  }       
  public T getFirstParam() {        
    return firstParam;    
  }       
  public void setFirstParam(T firstParam) {        
    this.firstParam = firstParam;    
  }       
  public S getSecondParam() {        
    return secondParam;    
  }       
  public void setSecondParam(S secondParam) {        
    this.secondParam = secondParam;    
  } 
}

The usage can be done as below:

MultiGenericParam<String, String> aParam = 
  new MultiGenericParam<String, String>("value1", "value2"); 
MultiGenericParam<Integer, Double> dayOfWeekDegrees = 
  new MultiGenericParam<Integer, Double>(1, 2.6); 

No primitives

Type parameters in Java can be classes, interfaces, enumerations and arrays of them, but not primitive data types. This limits the possibilities, but since autoboxing is available, it is acceptable. And if null is in Param<Integer>, unboxing at runtime will result in a NullPointerException.

Term Example
generic type Param
Type variable or formal type parameter T
parameterized type Param
actual type parameter Long
Raw type Param

Type Inference

Sometimes it is possible to find some type information automatically from the arguments passed to methods or constructors. This ability is known as type inference and may

  • Determine the types of the arguments.
  • Determine the type that the result is being assigned, or returned (if available).
  • Find the most specific type that works with all of the arguments.

Diamond operator

When initializing a variable whose type is generic, it is noticeable that the type parameter must be specified twice. With nested generics the overtime is unpleasant. Let's take a list containing maps, where the associative memory connects date values with strings:

List<Map<Date,String>> listOfMaps;
listOfMaps = new ArrayList<Map<Date,String>>();

The type parameter Map<Date, String> is placed once on the side of the variable declaration and once behind the keyword new.

If the compiler has all type information, the generic type parameters can be omitted after new, and only a pair of angle brackets remains:

// Instead of
List<Map<Date,String>> listOfMaps = new ArrayList<Map<Date,String>>();
// is possible:
List<Map<Date,String>> listOfMaps = new ArrayList<>();

The fact that the compiler can derive the types from the context is based on a compiler property called type inference. Because of the appearance of the angle brackets <>, the type represented by the angle brackets is also called diamond type. The pair <> is also called diamond operator, and it is an operator because it finds out the type, which is why it is also called diamond type inference operator.

Generic interfaces

An interface can be declared as a generic type just like a class. Let's have a look at the interfaces java.lang.Comparable and a section of java.util.Set (interface that prescribes operations for set operations, more on this in chapter 16, "Introduction to Data Structures and Algorithms").

Interface Comparable

public interface Comparable<T> { 
  int compareTo(T o);
}
public interface Set<E> extends Collection<E> {
    boolean add(E e);
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);
}

As known, the methods use the type variables T and E. With Set you can also see that it extends a generically declared interface itself.

When using generic interfaces the following two usage patterns can be derived:

  • A non-generic class type resolves generics during implementation.
  • A generic class type implements a generic interface and passes on the parameter variable.

Non-generic class type resolves generics during implementation

In the first case, a class implements the generically declared interface and specifies a concrete type. All numeric wrapper classes implement for example Comparable and fill the type parameter exactly with the type of the wrapper class:

public final class Integer extends Number implements Comparable<Integer> {
    public int compareTo( Integer anotherInteger ) { ... }
}
Complex generic types can easily be simplified by own type declarations. For example, instead of repeatedly writing
HashMap<String,List<Integer>>
// an abbreviation can be taken:
class StringToIntListMap extends HashMap<String,List<Integer>> {}

Bounded Type Parameters

There may be times when you want to limit the kinds of types that can be transferred to a type parameter. For instance, a method that works on numbers may only want to accept instances of numbers or their subclasses. This is what the parameters of the limited type are for.

List the name of the type parameter to declare a bounded type parameter, followed by the extends keyword, followed by its upper bound which is Number in this example. Note that extensions are generally used in this context to mean either 'extends' (as in classes) or 'implements' (as in interfaces).

public class Box<T> {
    private T t;          
    public void add(T t) {
        this.t = t;
    }
    public T get() {
        return t;
    }
    public <U extends Number> void inspect(U u){
        System.out.println("T: " + t.getClass().getName());
        System.out.println("U: " + u.getClass().getName());
    }
    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<Integer>();
        integerBox.add(new Integer(10));
        integerBox.inspect("some text"); // error: this is still String! 
    }
}

By modifying our generic method to include this bounded type parameter, compilation will now fail, since our invocation of inspect still includes a String:

Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
  be applied to (java.lang.String)
                        integerBox.inspect("10");
                                         ^
1 error

To specify additional interfaces that must be implemented, use the & character, as in:

<U extends Number & MyInterface>

Benefits of Generic class

Using generics has many benefits over non-generic code. In the following some of these benfits are presented.

Stronger type checks at compile time

A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find.

Elimination of casts

The following code snippet without generics requires casting:

List list = new ArrayList(); 
list.add("hello");
String s = (String) list.get(0);
When re-written to use generics, the code does not require casting:
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);   // no cast    

Enabling programmers to implement generic algorithms

By using generics, programmers can implement generic algorithms that work on collections of different types, can be customized, and are type safe and easier to read.

Exercise

  • Exercise 1:
    • Write a generic method to count the number of elements in a collection that have a specific property (e.g., odd integers, prime numbers, palindromes).
  • Exercise 2:
    • Write a generic method to exchange the positions of two different elements in an array.
  • Exercise 3:
    • Write a generic method to find the maximal element in the range [begin, end] of a list.