Polymorphic (Generic) Programming in Java
Polymorphic (Generic) Programming in Java
We have used the fact that Java classes are arranged as a tree with the built in class Object
at the root to write generic or polymorphic code such as the following function to find an
element in an array:
What if we wanted to write a similar program to copy the contents of one array into
another? A first attempt could be the following:
However, notice that it is not enough for the two arguments to be compatible with Object
for this function to work correctly: we also need the type of the array tgt to be a supertype
of the array src for the assignment to work. In other words, assuming that we have a class
Ticket with ETicket as a subclass, the following call is legal
because it is legal to assign ETicket objects to variables of type Ticket. However, the
converse call
arraycopy(ticketarr,elecarr);
would be flagged as a type error at runtime. In general, it is desirable to catch all type errors
at compile time, but this kind of error cannot be detected because we have no mechanism
to express dependencies across types in polymorphic code.
Let us look at another situation where our existing mechanism for typing is inadequate.
Suppose we want to define a linked list that can hold values of any type.
We could begin with a private class to store a node of the list described as follows:
1
private class Node {
public Object data;
public Node next;
...
}
We can then define our list as follows:
public class LinkedList{
public LinkedList(){
first = null;
}
if (first != null){
returnval = first.data;
first = first.next;
}
return returnval;
}
...
}
There are two points to note about this style of implementing a polymorphic list:
1. The implementation loses information about the original type of the element. All data
is stored and returned as an Object. This means we have to cast the output of head
even when it is clear what the return type is, as shown below:
2
LinkedList list = new LinkedList();
Ticket t1,t2;
t1 = new Ticket();
list.insert(t1);
t2 = (Ticket)(list.head()); // head() returns an Object
2. This implementation does not capture our intuitive understanding that the underlying
type of the list is uniform. There is nothing to stop us from constructing a heteroge-
neous list as follows:
Once again, the bottleneck is our inability to specify dependency across types within
the list: we would like to say that nodes can hold any type of data but the types across
all nodes in a list are the same.
One way of addressing these deficiencies is to introduce explicit type variables. This has
been incorporated in Java relatively recently (version 1.5) under the name generics.
1 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 T head(){
T returnval;
...
return returnval;
3
}
...
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.
4
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:
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
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.
This, however, does not define any constraints on the types assigned to S and T. We want
S to be a subtype of T, so we can make our definition more precise by writing2 :
2
In this context, Java does not distinguish between S implementing an interface T and S extending a class
T. For both cases, we just write S extends T.
5
public <S extends T,T> void arraycopy (S[] src, T[] tgt){
...
}
As another example, suppose we want to relax our typing constraint for lists to allow a
list of type T to store elements of any subtype of T. We could then define insert as follows:
public class LinkedList<T>{
...
}
Now we cannot be sure, in general, about the specific type of each element of the list.
However, we do still know that all values in the list are compatible with type T. Thus, the
definition and type of the function head remain the same as before:
public T head(){
T returnval;
...
return returnval;
}
6
This generates a type error at runtime. The compiler cannot find fault with the assign-
ment
However, since the hierarchy on base types is not inherited by LinkedList, LinkedList<Object>
is not the most general type of LinkedList. Instead, if we want a function that works on
all variants of LinkedList, we should introduce a type variable:
This means that the following version of the function classequal (see page 83 in the
older notes) would not work:
7
However, we can fruitfully exploit the fact that Class now has a type parameter. Suppose
we want to write a reflective function that takes as input a class and creates a new object of
that type. In the normal setup, we would have written:
The return type of this function is Object because we cannot infer any information about
the nature of the class c which is received as the argument.
In the revised framework, we if c is the Class object corresponding to a class T, then it
is actually of type Class<T>. Thus, we can rewrite this function as follows:
Now, notice that we are able to extract the underlying type T from c and use it to specify
a more precise return value, thus avoiding the need to use a cast when calling this function.
One important point about Javas parameterized classes is that they are more sophisti-
cated than macros or templates. It is not correct to think of a definition like
as a skeleton that is instantiated into a real Java class each time it is actually used
with a concrete value of T. There is truly only one class LinkedList<T> and all specific
instantiations of LinkedList<T> have the same underlying object. In other words, if we
declare
then
classequal(stringlist,intlist)
returns true.
This seems to have some unfortunate implications. For instance, suppose we write:
Since all instantiations LinkedList<T> are of the same type, the value returned by
sl.getClass() is of the generic type LinkedList<T> and the Java compiler is unable
to reconcile this with the specific type LinkedList<String> defined for newsl. (Try this
for yourself and see.)