Java Specification Request (JSR) 14 proposes to introduce generic types and methods to the Java programming language. Since the advent of Java, developers have been begging for the addition of generics to the language. It is the number one requested Request for Enhancement (RFE) on the Java Developer Bug Parade. Generics have been used in other programming languages for years and now they will be part of the Java 1.5 "Tiger" release due out some time at the end of 2003.
What are generics? They go by
other names that you have probably heard before such as parameterized
types or templates. They allow a programmer to work with
general, reusable classes (such as java.util.List, java.util.Map)
in a type-safe manner.
The two major benefits of generics in Java are:
To understand why generics are needed let's look at the sample program below:
class DangerousCast {
public static void main(String[] args) {
Stack stack = new Stack();
stack.push(new Integer(1));
stack.push("2");
stack.push(new Integer(3));
while(!stack.isEmpty()) {
Integer integer = (Integer) stack.pop();
. . . .
}
}
}
This example program
demonstrates a common source of bugs when programming in Java, a ClassCastException
caused by an invalid cast. Every cast in a program has the potential to
cause a ClassCastException at runtime but oftentimes they
are unavoidable when programming in Java.
With generics, this program
can be changed so that the above error will be caught at compile time.
You'll notice the additional type information in brackets (Stack<Integer>)
in the generics code; this is the way of telling the compiler the type
of object that the generic container will contain.
class CompilerSavesTheDay {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<Integer>();
stack.push(new Integer(1));
stack.push("2"); // Compiler Error generated by this call.
stack.push(new Integer(3));
while(!stack.isEmpty()) {
Integer integer = stack.pop();
. . . .
}
}
}
The compiler will issue an
error telling us that we cannot add a String to a Stack
that we have designated to only contain Integer objects.
The addition of generics to Java allows the compiler to detect bugs
that normally would go uncaught until runtime and are difficult to
debug.
Another benefit of generics is
code clarity. With the use of generics a method's parameters and/or
return types can be much more expressive then previously possible.
Let's examine a method declaration for a Customer with
and without the use of generics.
public Vector getAccounts();
public Vector<Account> getAccounts();
When you see the method that
uses generics you can be sure that you will be getting back a Vector
of Account objects because the compiler is enforcing
it.
Readability is improved since
generic classes reduce the number of casts dispersed throughout your
program. For example, let's take a look at a code sample where we have
a LinkedList that contains a LinkedList of String
objects. Here's how this code would look without the use of generics.
// Instantiate the data structure List list = new LinkedList(); list.add(new LinkedList()); // Add a value to it. ((List) list.get(0)).add("value"); // Retrieve the value String value = (String) ((List) list.get(0)).get(0);
Let's look and see how much cleaner the code sample above looks utilizing generics.
// Instantiate the data structure. List<List<String>> list = new LinkedList<List<String>>(); list.add(new LinkedList<String>()); // Add a value to it. list.get(0).add("value"); // Retrieve the value. String value = list.get(0).get(0);
Another feature that is being
introduced in Java 1.5 is autoboxing/unboxing of primitive
types (such as int, boolean) to their
respective reference type (such as Integer, Boolean).
This feature is not directly related to the addition of generics, but
it is worth noting since it will also improve code clarity by
eliminating the laborious activity of converting between primitive
types and wrapper types. Here's a small code sample that shows how
monotonous it can be to use an ArrayList to store ints
without autoboxing/unboxing.
List intList = new ArrayList();
for (int i=0; i < 10; i++) {
// Not how we have to wrap every int as an Integer intList.add(new Integer(i)); } int sum = 0; for (int i=0; i < intList.size(); i++) { // Note how we have to retrieve an Integer from the List
// and invoke intValue() to get an int. int num = ((Integer) intList.get(i)).intValue(); sum += num; }
Here's the same code with the use of generics and autoboxing/unboxing.
List<Integer> intList = new ArrayList<Integer>();
for (int i=0; i < 10; i++) {
// Note how we do not have to wrap the int into an Integer intList.add(i); } int sum = 0; for (int i=0; i < intList.size(); i++) { // Note how we can get the int directly from the List. sum += intList.get(i); }
For the remainder of this article, all code samples for generics will also utilize autoboxing/unboxing.
The addition of generics introduces parameterized types to Java. Parameterized types allow you to develop a class that can be generic in its implementation and yet be used in a very specific manner.
The classes that make up the Collections API will be generic compatible with the release of Java 1.5. What this means is that all classes in the Collections API have been modified to accept generic type parameters. A program that uses classes in the Collections API without generic type parameters will continue to work but the compiler will generate a warning stating that unsafe or unchecked operations are being used. We have seen a few examples above using generic Collections but let's look at an example that touches on it a little more.
// Declare a Map that accepts Integer keys and String values. Map<Integer, String> months = new HashMap<Integer, String>(); months.put(1, "January"); months.put(2, "February"); months.put(3, "March"); .... String month = months.get(1); // returns "January" // Declare a List of String values. List<String> days = new ArrayList<String>(); days.add("Sunday"); days.add("Monday"); days.add("Tuesday"); .... // Define a custom Comparator that will cause descending ordering // for String objects in a sort routine. Comparator<String> comparator = new Comparator<String>() { public int compare(String s1, String s2) { // Ignore null for brevity return -s1.compareTo(s2); } }; // Sort the days List in descending order. Collections.sort(days, comparator); String day = days.get(0); // returns Wednesday // This code still works but generates a compiler warning. List uncheckedDaysList = new ArrayList(); uncheckedDaysList.add("Sunday"); uncheckedDaysList.add("Monday"); uncheckedDaysList.add("Tuesday"); String uncheckedDay = (String) uncheckedDaysList.get(0);
Now that we have seen how to utilize generics in working with the Collections API, let's see how we can use generics for classes and interfaces that we develop.
Many of us have likely
developed a Pair class that holds a heterogeneous pair of
objects for different projects we have worked on. Before generics this
is what our Pair class would look like (note the sample
usage in the main method).
public class Pair {
private Object first;
private Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return this.first;
}
public Object getSecond() {
return this.second;
}
public static void main(String[] args) {
// month number, month name pair
Pair jan = new Pair(new Integer(1), "January");
int monthNum = ((Integer) jan.getFirst()).intValue();
String monthName = (String) jan.getSecond();
// is winter flag, month name pair
Pair dec = new Pair(Boolean.TRUE, "December");
boolean isWinter = ((Boolean) dec.getFirst()).booleanValue();
monthName = (String) dec.getSecond();
}
}
This class works fine but
introduces many casts into our code. Previous examples demonstrated
that casts are dangerous, ugly, and should be avoided where possible.
With the use of generics we can define a type-safe Pair
class. Here's the code with sample usage in the main
method.
public class Pair<T1, T2> {
private T1 first;
private T2 second;
public Pair(T1 first, T2 second) {
this.first = first;
this.second = second;
}
public T1 getFirst() {
return this.first;
}
public T2 getSecond() {
return this.second;
}
public static void main(String[] args) {
// month number, month name pair
Pair<Integer, String> jan =
new Pair<Integer, String>(1, "January");
int monthNum = jan.getFirst();
String monthName = jan.getSecond();
// is winter flag, month name pair
Pair<Boolean, String> dec =
new Pair<Boolean, String>(true, "December");
boolean isWinter = dec.getFirst();
monthName = dec.getSecond();
}
}
Generic interfaces can be defined just as easily as generic classes can. Here's a generic interface for object pooling.
public interface ObjectPool<T> {
public T getPooledObject();
public void releasePooledObject(T obj);
}
In all of the examples thus far we have not had the need to restrict the type of the parameter(s) for the generic classes/interfaces we have used. The generics syntax allows us to enforce a parameter to extend a certain class and/or implement a set of interfaces. Restricting the type of parameter for a generic class/interface/method is referred to having a bound parameter.
Here is an example of using a
bound parameter in a generic class definition. In this example we want
to implement an extension of java.util.ArrayList that
only accepts Number types.
public class MyNumberList<T extends Number> extends java.util.ArrayList<T> {
public double sum() {
....
}
public double average() {
....
}
}
So the following declarations are legal:
MyNumberList<Double> myNumberList = new MyNumberList<Double>();
MyNumberList<Integer> myNumberList2 = new MyNumberList<Integer>();
But the following declarations are not legal:
MyNumberList<String> myStringList = new MyNumberList<String>();
MyNumberList<Boolean> myBooleanList = new MyNumberList<Boolean>();
Just as classes and interfaces
can be generic, methods can also take generic type parameters. The
parameter section of the method precedes the method's return type.
Let's look at how we would define a method for a sorting routine we
have implemented that will work on a Collection of Comparable
objects.
public static <T extends Comparable> void mySortRoutine(Collection<T> collection);
The section <T
extends Comparable> is the method's type parameter, it states
that the Collection that is passed as a parameter to our
method must contain Comparable objects.
As an alternative to our NumberList
class declared above, we could write a utility class utilizing generic
methods to provide the same functionality, note the sample usage in the
main method.
public class NumberCollectionUtils {
public static <N extends Number> double sum(Collection<N> coll) {
....
}
public static <N extends Number> double average(Collection<N> coll) {
....
}
public static void main(String[] args) {
List<Double> myDoubleList = new ArrayList<Double>();
myDoubleList.add(4.0);
myDoubleList.add(5.0);
System.out.println("sum is " + sum(myDoubleList)); // prints 9.0
System.out.println("average is " + average(myDoubleList)); // prints 4.5
List<Integer> myIntList = new ArrayList<Integer>();
myIntList.add(5);
myIntList.add(6);
System.out.println("sum is " + sum(myIntList)); // prints 11.0
System.out.println("average is " + average(myIntList)); // prints 5.5
}
}
Type variables are allowed in
a throws clause on a method signature. This allows being
generic about what Exception can be thrown by a method
so that a client can be specific about what Exception to
catch. Here's an example:
public class ExceptionTest {
// A generic interface that takes a bound type parameter
// of Exception. It uses this type parameter in the
// throws clause of its service method.
static interface Service<E extends Exception> {
public void service() throws E;
}
// A class that has a generic method for running an implementation
// of the Service interface. The generic parameter for this method
// is the type parameter needed for the generic Service interface.
static class ServiceRunner {
static <E extends Exception> void run(Service<E> service) throws E {
service.service();
}
}
public static void main(String[] args) {
try {
// Causes the running of a Service implementation
// that will perform some type of IO operation thus
// needing to handle an IOException.
ServiceRunner.run(new Service<java.io.IOException>() {
public void service() throws java.io.IOException {
// Perform some type of IO operation
}
});
} catch (java.io.IOException ex) {
// Do something useful
}
}
}
JSR014 stemmed from the Generic Java (GJ) Proposal. The addition of generics will be backward compatible with existing code, thus allowing an easier retrofitting of existing code to use generics syntax.
Generics in Java are
implemented using a type erasure mechanism. The compiler translates all
type parameters in the source code to their bounding type in the class
file. A type parameter's bounding type is Object if a
bound type is not specified. For example,
class NotBoundClass<T> { .... }
would have a bounding type of Object while
class BoundClass<T extends javax.swing.JComponent>
has a bounding type of JComponent. The compiler is also
responsible for inserting casts during the translation phase. The
compiler guarantees that any cast inserted during this translation
phase will not fail. Here are a few of the major
benefits of the Java generics implementation.
It is important to note that since casting is still being performed behind the scenes, your Java programs will not receive a performance boost from using generics. The addition of generics to the Java language is not intended to improve performance, it is intended to increase program readability and reliability while maintaining backwards compatibility with existing programs.
A notable limitation of the
generics implementation is that only reference types are allowed as
type parameters, therefore primitive types are not allowed as type
parameters. So if we want:
Map<int, String> = new HashMap<int, String>;
we have to write:
Map<Integer, String> = new HashMap<Integer, String>;
Although primitive types are not allowed as type parameters, the new
autoboxing/unboxing feature that is discussed above will make this
limitation relatively unnoticeable.
Using type erasure at translation time for generics sometimes leads to the compiler having to insert bridge methods in the compiled byte code. Let's take a look at an example where bridge methods are needed.
interface java.util.Comparator<A> {
public int compare(A x, A y);
}
class MyStringComparator implements Comparator<String> {
public int compare(String x, String y) {
....
}
}
Here's the translated byte code for the above class definition.
interface java.util.Comparator {
public int compare(Object x, Object y);
}
class MyStringComparator implements Comparator {
public int compare(String x, String y) {
....
}
// Bridge method inserted so that the
// interface is properly implemented
public int compare(Object x, Object y) {
return compare((String) x, (String) y);
}
}
The compiler must introduce
the bridge method, since overriding cannot occur because the parameter
types for the compare method do not match exactly. The
compiler can insert the bridge method without any problems because of
Java's support for method overloading.
Another new feature of Java 1.5 is support for covariant return types. Support for covariant return types will allow a method overriding a super class method to return a subclass of the super class method's return type. This feature is not directly related to the addition of generics but is worth noting because it is something the developer community has been requesting for a long period of time. This feature is mentioned in this article because they are implemented as a side effect of the generics implementation. Support for covariant return types is implemented using bridge methods inserted by the compiler similar to those that are inserted for generic classes
For example, the cloning is
typically implemented by overriding the clone method from
Object. The only problem with this is that we cannot
change the return type from Object to our Cloneable
class because Java (before 1.5) does not allow for covariant return
types. Let's take a look at how we would implement cloning before
covariant return types.
public class CloneableClass {
....
public Object clone() {
....
}
public static void main(String[] args) {
CloneableClass oldWay = new CloneableClass();
CloneableClass clone = (CloneableClass) oldWay.clone();
}
}
You'll notice that although we
are calling clone on an instance of CloneableClass
we still have to cast the return type to CloneableClass,
this seems like too much work and also could lead to a ClassCastException
if we accidentally cast to the incorrect type. Let's look at
how covariant return types make this process simpler and less error
prone:
public class CloneableClass {
....
public CloneableClass clone() {
....
}
public static void main(String[] args) {
CloneableClass newWay = new CloneableClass();
CloneableClass clone = newWay.clone();
}
}
Covariant return types are
just another feature that adds clarity and type safety to Java. Because
of covariant return types it makes it clear that when we call clone
on a CloneableClass instance we are going to get back a CloneableClass
and not some arbitrary type.
If you would like to start writing code that utilizes generics before Java 1.5 is released, you can download a prototype compiler for JSR014 from Sun's website. You will also find it useful to download the generics specification. Note that you will need to be a member of the Java Developer connection to download the prototype and the specification. Keep in mind that it is a prototype compiler and the specification may still undergo minor changes, so don't go out and rewrite your production application to support generics using the prototype. It is useful for those of you who wish to get a jumpstart on generics and want to do a little experimenting.
Here is a list of tools with generics support:
The pending Java 1.5 release will bring some very major changes to the Java programming language and the most significant amongst these changes is the addition of generics. Generics will greatly improve program clarity by reducing the number of casts dispersed throughout source code. Generics will increase program reliability by adding compile time type checking. Learning how to use generics should be relatively straightforward, hopefully this article has inspired you to look deeper into how you can use them to improve the programs you write.