9
FEBRUARY, 2005
Delving further with Java generics
By John Hunt
Introduction
In an earlier article, we looked at the basics of the new Generics language
features added to Java with Java 2 Platform Standard Edition 5.0 Development
Kit (aka JDK 5.0). However, we stopped short of delving down into some
of the more esoteric looking aspects of Java Generics. Nevertheless, although
the language features discussed here might look somewhat unusual, they
have a very important part to play in the real world benefits that generics
can offer.
In
this article, we will look at the relationship between generic lists of
one-type and generic lists of another type of Java object. We will consider
how they are and are not related and what the implications are for developers.
This will lead us onto consider the use of wild cards and later bounded
wildcards for generic variables.
Generics and their types
In the last article, we looked at how we can use generics with collections.
We noted that generics allow a particular collection to be limited to
a particular type of object. For example, we were able to write:
List<String>
list = new ArrayList<String>();
This created
an instance of the class ArrayList
that is limited to hold only string objects. Thus later on when we retrieve
elements within the array list we (and Java) will know that they are strings.
For example:
Iterator<String>
it = list.iterator();
while (it.hasNext()) {
String st = it.next();
System.out.println(st);
}
We also noted
that what is created when you make an instance of a collection using generics
is an instance of the appropriate concrete class. Thus, if we consider
the following:
List<Integer>
intList = new ArrayList<Integer>();
List<String> stringList = new ArrayList<String>();
Both the
instances created are instances of the class ArrayList,
it is just that intList
is configured to hold instances of the class Integer and stringList
is configured to hold instances of the class String. All well and good,
but what about the following:
List<String>
stringList = new ArrayList<String>();
List<Object> objectList = stringList;
What is the
result of the above? Well at first sight, this may appear completely legal.
After all what are we storing into the variable objectList?
In practical terms, we are storing a reference to an instance of ArrayList.
ArrayList is a class that implements the List
interface and thus a variable of type List
should be able to reference an instance of type ArrayList!
Also, if we take this further, a String
is a type of Object
– remember Java is a single rooted class hierarchy language and
the class Object
is the root of all class hierarchies thus a String inherits
from Object!
Of
course, the above sounds quite intuitive but is sadly misleading. The
statement:
List<Object>
objectList = stringList;
Is very dangerous
and is not something we would want the language to allow and indeed Java
does not allow it. This statement would in fact lead a compiler error:
source>javac
GenericsExample.java
GenericsExample.java:11: incompatible types
found : java.util.List<java.lang.String>
required: java.util.List<java.lang.Object>
List<Object>
objectList = stringList;
^
1 error
So, the compiler
has specified that objectList
and stringList
are incompatible types and I have said that we really don’t want
to allow them to be the same. So, what is going on and why is it happening?
The answer comes when you think about what might be written if we were
allowed to assign stringList
to objectList:
Object obj
= new Object();
objectList.add(obj);
String st = stringList.get(0);
This would
allow us to add a new instance of the object class to the list referenced
by objectList
(which is of course the same list as is referenced by stringList).
The net effect would be that stringList
now held an object and not a string and thus the last line above would
be illegal.
To
avoid this situation from occurring, you cannot assign a generic collection
of one type to a variable for a generic collection of another type (even
if the two types are related by inheritance). This may go against all
that we hold dear regarding classes, interfaces and inheritance but it
makes practical sense.
Generics
and Wildcards
Okay you may say – but what happens if I want to pass a generic
collection into a method? For example, pre-JDK 5.0 I might well have written:
import java.util.Iterator;
import java.util.List;
public class
EmployeeListExample {
public void printPayslips(List employees) {
Iterator it = employees.iterator();
while (it.hasNext())
{
printPayslip((Employee)it.next());
}
}
}
This code
takes a list that contains Employee objects (or instances of subclasses
of Employee). It iterates over this list passing each employee to the
printPayslip
method.
In
JDK 5.0, I might want to modify this to use generics (then I can guarantee
the contents of the list). For example, I might want to write the following:
import java.util.Iterator;
import java.util.List;
public class
EmployeeListExample {
public void printPayslips(List<Employee>
employees) {
Iterator<Employee> it
= employees.iterator();
while (it.hasNext()) {
printPayslip((it.next());
}
}
}
This will
work fine given what we know about generics – that is, as long as
we pass it a list that has been instantiated to hold Employees.
However, what about if we have a list of Managers
(and we assume that Managers are a subclass of Employee)?
We
already know that a List instantiated with the Employee
class is not equal to a list instantiated with the Manager
class (even if Manager
is a direct subclass of Employee). Thus, we cannot pass in a Manager
List to the printPayslips
method. This seems something of a retrograde step, compared to the non-generic
version. In the original version, we could pass in any type of list holding
any type of object (even if this caused us problems later on).
However,
there is a way around this – we can use a wildcard. That is, we
do not specify at compile time the type of class used to instantiate the
generic list. Instead, we allow it to be determined at runtime. This is
written as:
List<?>
This is read
as a “List of Unknown” type. The revised version of the printPayslips
method would now resemble:
public void
printPayslips(List<?> employees) {
Iterator<?>
it = employees.iterator();
while
(it.hasNext()) {
printPayslip(it.next());
}
}
Note however,
that the iterator must also be specified with the wildcard. Now we can
pass in an ArrayList
of Managers. Note, however, that the type of the object returned from
the it.next()
method would be object. Thus if the printPayslip()
method takes an Employee, then a cast will be needed.
Bounded Wildcards
The example above helps allow any type of generic list to be given to
a printPaySlips
method. However, if we examine this code and compare it with the non-generic
version we find that we have actually undone everything we wanted generics
to do. That is, we now allow any type of generic list to be given to the
printPayslips
method (it can contain employees, Strings,
or indeed any type of object). The object returned from it.next()
now has a type of object and we would need to cast it to Employee if printPayslip
took an Employee
(or subclass of Employee).
So, what is the use of this other than to make this method work with generics?
Actually, the point is we can now consider what are referred to as Bounded
Wildcards. This version of a wild card allows us to specify that any collection
or list instantiated for a particular type or subtype may be passed in.
Thus, we can say that a list of employees or any subclass of employees
may be passed into the printPayslips
method. This version of the method now looks like this:
import java.util.Iterator;
import java.util.List;
public class
EmployeeListExample {
public void printPayslips(List<? extends
Employee> employees) {
Iterator<? extends
Employee> it = employees.iterator();
while (it.hasNext())
{
printPayslip(it.next());
}
}
public void printPayslip(Employee emp) {
System.out.println(emp);
}
}
We now have
a method that can take a list of Employees
or Managers
and will return an object of type Employee
from the it.next()
method (even if it is actually a subclass of Employee).
We have regained the type safe advantages of generics without tying the
implementation to a particular type of class!
|