Generics in Java

We can now introduce type parameters for class definitions by using type variables. For instance, here is how we could modify our list definition to fix an arbitrary but uniform type T for the data stored in the list.

  public class LinkedList<T>{

    private int size;
    private Node first;

    ...
    
    public T head(){
      T returnval;
      ...
      return returnval;
    }
    
    public void insert(T newdata){
      ...  // code to insert newdata in list
    }
    
    ...

    private class Node {
      public T data;
      public Node next;
      ...
    } 

  }

The parameter T in angled brackets denotes a type variable for the class LinkedList. An instance of LinkedList would have T bound to a fixed class: for instance, LinkedList<Ticket> or LinkedList<Date>. All references to T inside the class definition are bound to the value of T used when the class was instantiated. Here is an example of how we would define and use the new type of linked list.

     LinkedList<Ticket> ticketlist = new LinkedList<Ticket>();
     LinkedList<Date> datelist = new LinkedList<Date>();
     Ticket t = new Ticket();
     Date d = new Date();
     ticketlist.insert(t);
     datelist.insert(d);
     ...

When we invoke ticketlist.insert(t), the type of t is compared by the compiler to the instantiation of the type T used when defining ticketlist. In this case, since ticketlist was created as an object of type LinkedList<Ticket>, T is bound to Ticket and so it is legal to invoke ticketlist.insert(t) for a Ticket object t. On the other hand, ticketlist.insert(d) for a Date d would be a type error. It is important to note that this type compatibility can be checked at compile time. 1

Notice that all instances of T within the parameterized class definition are automatically bound once the class is created with a specific value of T. Thus, the return type T in the function head and the type T for the field data in the private class Node both get instantiated correctly to the actual type provided for T when the class is instantiated.

We can also define type parameters for functions within a class. For instance, suppose we want to rewrite arraycopy in a very strict form that insists that both the source and target arrays are of the same type. We could specify this as follows:

  public <T> void arraycopy (T[] src, T[] tgt){
    int i,limit;
    limit = min(src.length,tgt.length); // No overflows!
    for (i = 0; i < limit; i++){
        tgt[i] = src[i];
    }
  }

The type parameter comes before the return type. Since T is substituted uniformly by the same type value, both src and tgt must be arrays of the same type.

It is important to note the difference between the definitions of arraycopy and head (from LinkedList). In the function head, we used a type variable T without defining it as part of the function definition. This is because the T that appears in head is derived from the parameter T supplied to the surrounding class. If we write, instead

    public <T> T head(){
      T returnval;
      ...
      return returnval;
    }

then the type variable T referred to inside head is a new, private variable which hides the outer T and is completely independent. In other words, we should think of <T> like a logical quantifier with a natural scope. If we reuse <T> within the scope of another <T>, the outer T is hidden by the inner T.

Madhavan Mukund 2006-02-19