Generics in Java

Generics in Java

By Rob Smith, OCI Senior Software Engineer

July 2003


Introduction

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.Listjava.util.Map) in a type-safe manner.

Why are they needed?

The two major benefits of generics in Java are:

  1. Reducing the number of casts in your program, thus reducing the number of potential bugs in your program.
  2. Improving code clarity

Reducing Casts

To understand why generics are needed let's look at the sample program below:

  1. class DangerousCast {
  2.  
  3. public static void main(String[] args) {
  4. Stack stack = new Stack();
  5. stack.push(new Integer(1));
  6. stack.push("2");
  7. stack.push(new Integer(3));
  8.  
  9. while(!stack.isEmpty()) {
  10. Integer integer = (Integer) stack.pop();
  11. . . . .
  12. }
  13. }
  14.  
  15. }

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) in the generics code; this is the way of telling the compiler the type of object that the generic container will contain.

  1. class CompilerSavesTheDay {
  2.  
  3. public static void main(String[] args) {
  4. Stack<Integer> stack = new Stack<Integer>();
  5. stack.push(new Integer(1));
  6. stack.push("2"); // Compiler Error generated by this call.
  7. stack.push(new Integer(3));
  8.  
  9. while(!stack.isEmpty()) {
  10. Integer integer = stack.pop();
  11. . . . .
  12. }
  13. }
  14. }

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.

Improving Code Clarity

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);

Autoboxing/unboxing

Another feature that is being introduced in Java 1.5 is autoboxing/unboxing of primitive types (such as intboolean) to their respective reference type (such as IntegerBoolean). 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.

  1. List intList = new ArrayList();
  2.  
  3. for (int i=0; i < 10; i++) {
  4. // Note how we have to wrap every int as an Integer
  5. intList.add(new Integer(i));
  6. }
  7.  
  8. int sum = 0;
  9. for (int i=0; i < intList.size(); i++) {
  10. // Note how we have to retrieve an Integer from the List
  11. // and invoke intValue() to get an int.
  12. int num = ((Integer) intList.get(i)).intValue();
  13. sum += num;
  14. }

Here's the same code with the use of generics and autoboxing/unboxing.

  1. List<Integer> intList = new ArrayList<Integer>();
  2.  
  3. for (int i=0; i < 10; i++) {
  4. // Note how we do not have to wrap the int into an Integer
  5. intList.add(i);
  6. }
  7.  
  8. int sum = 0;
  9. for (int i=0; i < intList.size(); i++) {
  10. // Note how we can get the int directly from the List.
  11. sum += intList.get(i);
  12. }

For the remainder of this article, all code samples for generics will also utilize autoboxing/unboxing.

Usage

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.

Using the Generic Collections API

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.

  1. // Declare a Map that accepts Integer keys and String values.
  2. Map<Integer, String> months = new HashMap<Integer, String>();
  3. months.put(1, "January");
  4. months.put(2, "February");
  5. months.put(3, "March");
  6. ....
  7. String month = months.get(1); // returns "January"
  8.  
  9. // Declare a List of String values.
  10. List<String> days = new ArrayList<String>();
  11. days.add("Sunday");
  12. days.add("Monday");
  13. days.add("Tuesday");
  14. ....
  15.  
  16. // Define a custom Comparator that will cause descending ordering
  17. // for String objects in a sort routine.
  18. Comparator<String> comparator = new Comparator<String>() {
  19. public int compare(String s1, String s2) { // Ignore null for brevity
  20. return -s1.compareTo(s2);
  21. }
  22. };
  23.  
  24. // Sort the days List in descending order.
  25. Collections.sort(days, comparator);
  26. String day = days.get(0); // returns Wednesday
  27.  
  28. // This code still works but generates a compiler warning.
  29. List uncheckedDaysList = new ArrayList();
  30. uncheckedDaysList.add("Sunday");
  31. uncheckedDaysList.add("Monday");
  32. uncheckedDaysList.add("Tuesday");
  33. String uncheckedDay = (String) uncheckedDaysList.get(0);

Defining Generic classes and interfaces

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).

  1. public class Pair {
  2. private Object first;
  3. private Object second;
  4.  
  5. public Pair(Object first, Object second) {
  6. this.first = first;
  7. this.second = second;
  8. }
  9.  
  10. public Object getFirst() {
  11. return this.first;
  12. }
  13.  
  14. public Object getSecond() {
  15. return this.second;
  16. }
  17.  
  18. public static void main(String[] args) {
  19. // month number, month name pair
  20. Pair jan = new Pair(new Integer(1), "January");
  21. int monthNum = ((Integer) jan.getFirst()).intValue();
  22. String monthName = (String) jan.getSecond();
  23.  
  24. // is winter flag, month name pair
  25. Pair dec = new Pair(Boolean.TRUE, "December");
  26. boolean isWinter = ((Boolean) dec.getFirst()).booleanValue();
  27. monthName = (String) dec.getSecond();
  28. }
  29.  
  30. }

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.

  1. public class Pair<T1, T2> {
  2. private T1 first;
  3. private T2 second;
  4.  
  5. public Pair(T1 first, T2 second) {
  6. this.first = first;
  7. this.second = second;
  8. }
  9.  
  10. public T1 getFirst() {
  11. return this.first;
  12. }
  13.  
  14. public T2 getSecond() {
  15. return this.second;
  16. }
  17.  
  18. public static void main(String[] args) {
  19. // month number, month name pair
  20. Pair<Integer, String> jan =
  21. new Pair<Integer, String>(1, "January");
  22. int monthNum = jan.getFirst();
  23. String monthName = jan.getSecond();
  24.  
  25. // is winter flag, month name pair
  26. Pair<Boolean, String> dec =
  27. new Pair<Boolean, String>(true, "December");
  28. boolean isWinter = dec.getFirst();
  29. monthName = dec.getSecond();
  30. }
  31. }

Generic interfaces can be defined just as easily as generic classes can. Here's a generic interface for object pooling.

  1. public interface ObjectPool<T> {
  2.  
  3. public T getPooledObject();
  4.  
  5. public void releasePooledObject(T obj);
  6. }

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.

  1. public class MyNumberList<T extends Number> extends java.util.ArrayList<T> {
  2.  
  3. public double sum() {
  4. ....
  5. }
  6. public double average() {
  7. ....
  8. }
  9.  
  10. }

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>();

Generic methods

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.

  1. public class NumberCollectionUtils {
  2.  
  3. public static <N extends Number> double sum(Collection<N> coll) {
  4. ....
  5. }
  6.  
  7. public static <N extends Number> double average(Collection<N> coll) {
  8. ....
  9. }
  10.  
  11. public static void main(String[] args) {
  12. List<Double> myDoubleList = new ArrayList<Double>();
  13. myDoubleList.add(4.0);
  14. myDoubleList.add(5.0);
  15. System.out.println("sum is " + sum(myDoubleList)); // prints 9.0
  16. System.out.println("average is " + average(myDoubleList)); // prints 4.5
  17.  
  18. List<Integer> myIntList = new ArrayList<Integer>();
  19. myIntList.add(5);
  20. myIntList.add(6);
  21. System.out.println("sum is " + sum(myIntList)); // prints 11.0
  22. System.out.println("average is " + average(myIntList)); // prints 5.5
  23. }
  24.  
  25. }

Exceptions

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:

  1. public class ExceptionTest {
  2.  
  3. // A generic interface that takes a bound type parameter
  4. // of Exception. It uses this type parameter in the
  5. // throws clause of its service method.
  6. static interface Service<E extends Exception> {
  7.  
  8. public void service() throws E;
  9.  
  10. }
  11.  
  12. // A class that has a generic method for running an implementation
  13. // of the Service interface. The generic parameter for this method
  14. // is the type parameter needed for the generic Service interface.
  15. static class ServiceRunner {
  16. static <E extends Exception> void run(Service<E> service) throws E {
  17. service.service();
  18. }
  19.  
  20. }
  21.  
  22. public static void main(String[] args) {
  23. try {
  24. // Causes the running of a Service implementation
  25. // that will perform some type of IO operation thus
  26. // needing to handle an IOException.
  27. ServiceRunner.run(new Service<java.io.IOException>() {
  28. public void service() throws java.io.IOException {
  29. // Perform some type of IO operation
  30. }
  31. });
  32. } catch (java.io.IOException ex) {
  33. // Do something useful
  34. }
  35. }
  36. }

Implementation

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.

Type Erasure Implementation

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.

Bridge Methods

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.

  1. interface java.util.Comparator<A> {
  2. public int compare(A x, A y);
  3. }
  4.  
  5. class MyStringComparator implements Comparator<String> {
  6. public int compare(String x, String y) {
  7. ....
  8. }
  9. }

Here's the translated byte code for the above class definition.

  1. interface java.util.Comparator {
  2. public int compare(Object x, Object y);
  3. }
  4.  
  5. class MyStringComparator implements Comparator {
  6.  
  7. public int compare(String x, String y) {
  8. ....
  9. }
  10.  
  11. // Bridge method inserted so that the
  12. // interface is properly implemented
  13. public int compare(Object x, Object y) {
  14. return compare((String) x, (String) y);
  15. }
  16.  
  17. }

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.

Covariant return types

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.

  1. public class CloneableClass {
  2. ....
  3.  
  4. public Object clone() {
  5. ....
  6. }
  7.  
  8. public static void main(String[] args) {
  9. CloneableClass oldWay = new CloneableClass();
  10. CloneableClass clone = (CloneableClass) oldWay.clone();
  11. }
  12.  
  13. }

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:

  1. public class CloneableClass {
  2. ....
  3.  
  4. public CloneableClass clone() {
  5. ....
  6. }
  7.  
  8. public static void main(String[] args) {
  9. CloneableClass newWay = new CloneableClass();
  10. CloneableClass clone = newWay.clone();
  11. }
  12.  
  13. }

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.

Ready to Get Started?

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.

Tools with Generics Support

Here is a list of tools with generics support:

Summary

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.

References



Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.


secret