Generics

Generics in Java allows classes or methods to be parameterized by a type. This enables code to be reused for multiple types without loosing type safety. Before introducing the syntax of generics, let us better understand the motivation behind generics using an example of a collection class (in fact as we have already seen, the Java collections framework makes heavy use of generics).

A motivating example - a generic List datastructure

Say we want to implement a linked list data structure as introduced previously in the course. We want to be able to create lists that store any elements of any class, but a single list should only store elements of a particular class, e.g., a String list should only store String objects. Since we do not know upfront what the class of elements will be the only way we can achieve this without the use of generics is to declare elements of be of class Object:

public class ListCell {
    Object element;
    ListCell next;
    ...
}

Then to ensure that only elements of a certain type can be stored, we can declare List to have a Class field storing this type. To check at runtime that new elements added to the List are of the right type, we have to resort to reflection as introduced in the previous notebook.

public class List {
    ListCell head;
    ListCell tail;
    Class type;

    public void append(Object newEl) throws Exception {
        ListCell newCell = new ListCell(newEl);
        if(!newEl.getClass().isAssignableFrom(type)) {
            throw new Exception("cannot add elements of type " + newEl.getClass() + " to a list of type " + type);
        ...
    }
}

This approach has several disadvantages:

  1. it results in a lot of boilerplate code
  2. the implementation will be quite slow since reflection is not very efficient in Java
  3. Type errors will be detected at runtime instead of at compile time

Generics syntax

Generics enables Java classes, methods, and fields to be parameterized by one or more types. These the names of these type parameters are given surrounded by <> after the class name, method name, or interface name, e.g.,

public class MyList<E> { ...

declares a class MyList with a type parameter E. When creating instances of this class we have to specify a value (class) for the type parameters, e.g., we can now create a MyList<String> or a MyList<Integer>. The type assigned to E is available within the classes body under its name (E). For instance, we can now declare methods that take parameters of type E as input, declare fields of type E, and so on.

Similarly, we can declare generic methods where the type parameter is specified after the modifiers of the method:

public <T> List<T> singletonList(T el);

Type erasure

Java uses a concept called type erasure to implement generics. To ensure type safety at compile time every usage of generic class, methods, and interfaces is checked for type safety, e.g., if we create a MyList<Integer> and then try to store an String in this list, then this will result in a compile time error. However, after types have been checks all type information is removed from the program and the generated Java byte code does not contain any type information anymore. While this has some advantages, it also results in limitations (see here).

Improving our Linked List with Generics

In [1]:
/**
 * 
 */
package lecture;

/**
 * @author lord_pretzel
 *
 */
public class MyList<E> {
    
    private ListCell head;
    private ListCell tail;
    
    private class ListCell {
        E value;
        ListCell next;
        
        public ListCell(E value) {
            this.value = value;
        }
    }
    
    public MyList () {
        head = null;
        tail = null;
    }
    
    public void append(E el) {
        ListCell n = new ListCell(el);
        if (head == null) {
            head = n;
            tail = n;
        }
        else {
            tail.next = n;
            tail = n;
        }
    }
    
    public String toString() {
        StringBuilder result = new StringBuilder();
        result.append("[");
        ListCell cur = head;
        while(cur != null) {
            result.append(cur.value.toString());
            if(cur.next != null)
                result.append(", ");
            cur = cur.next;
        }
        result.append("]");
        return result.toString();
    }
    
    public static void main (String[] args) {
        MyList<Integer> ints = new MyList<Integer> ();
        ints.append(1);
        ints.append(2);
        System.out.println(ints);
    }
}
Out[1]:
lecture.MyList

Using our generic list

In [3]:
import lecture.MyList;

MyList.main(null);
return null;
[1, 2]
Out[3]:
null
In [6]:
import lecture.MyList;

// create a list storing strings
MyList<String> l = new MyList<String>();
l.append("hello");
l.append(3); // cannot append strings, this will result in a compile time error!
return l.toString();
incompatible types: int cannot be converted to java.lang.String
 l.append(3)
          ^^  

Some messages have been simplified; recompile with -Xdiags:verbose to get full output

Pairs as Example

Let's create a class that can store pairs of objects containing an object of a type L and one of a type R.

In [7]:
package lecture;

public class Pair<L,R> {
    
    private L left;
    private R right;
    
    public Pair(L left, R right) {
        this.left = left;
        this.right = right;
    }
    
    public L getLeft() {
        return left;
    }
    
    public R getRight() {
        return right;
    }
    
    public String toString() {
        return "<" + left.toString() + "," + right.toString() + ">";
    }
}
Out[7]:
lecture.Pair
In [8]:
package lecture;

import lecture.Pair;

public class UsePairs {
    
    public static <L> Pair<L,L> pairWithItself(L it) {
        return new Pair<L,L> (it, it);
    }
    
    public static <L,R> Pair<R,L> switchPair(Pair<L,R> in) {
        return new Pair<R,L> (in.getRight(), in.getLeft());
    }
    
}
Out[8]:
lecture.UsePairs
In [9]:
import lecture.UsePairs;

return UsePairs.pairWithItself("hello");
Out[9]:
<hello,hello>
In [12]:
import lecture.UsePairs;
import lecture.Pair;

return UsePairs.switchPair(new Pair<Integer,String> (1, "hello"));
Out[12]:
<hello,1>

Wildcards

When using a generic class we may want to restrict the types of the classes type parameters, e.g., they should be a subclass of a certain class. In Java this can be achieved with wildcards.

Restricting the permissible types for type parameters

If we would have defined MyList as follows:

public class MyList<E extends Person> {
...

then only objects that are of instance of Person or one of its subclasses would be accepted for type parameters E

?

We can also restrict the allowable classes for a parameter of an generic class either requiring that it is a super or a subclass of a given class:

public void myMethod(List<? extends Person> x) { // method that takes a list of elements that are of a subclass of Person or of class Person as input
...
}
public void myMethod(List<? super Person> x) { // method that takes a list of elements that are of a superclass of Person or of class Person as input
...
}