Generics

The example programs are contained in generics.zip. Much of these notes are based on the discussion on generics in the Java Tutorials.

Motivation

What are generics? Why use generics?

The CastGenericExample program illustrates a key difference without and with generics. Java programs before generics used a lot of unsafe casting to process collections.

Generic Classes

A generic class is a class that is parameterized over types. The Java compiler uses generics to ensure type-safety. One of the key concepts of Java's generics is that only the compiler processes the generic parameters. There is no generic type-checking in the runtime code.

Several classes that store a pair of objects illustrate the concept. The PairTest program shows an example of using these classes.

The PairOfObjects class stores a pair of objects. However, casts are needed to use the objects that are stored. Classes for specific types of objects, such as PairOfIntegers and PairOfStrings, can be coded, but this results in many classes to maintain.

The PairOfSameType class includes a generic type parameter <T> right after the class name in the header:

 public class PairOfSameType<T>
      
Note how the code in this class is similar to PairOfObjects, PairOfIntegers, and PairOfStrings. All occurrences of Object, Integer, and String in these three classes been replaced with the generic type parameter T in a single class, PairOfSameType. One way to think of this is that the code is allowed to substitute T with any reference type, e.g., The Java compiler will ensure that the generic argument is used consistently.

One limitation of the PairOfSameType class is that both objects must be the same type. The PairOfDifferentTypes class resolves this limitation by including two generic type parameters. First, look at the PairOfIntegerAndDouble class for an example of a class that stores two objects of different types. The code in PairOfDifferentTypes substitutes Integer and Double with the generic type parameters S and T.

The Comparable and Comparator Interfaces

Suppose we want a City class which stores the name of the city and the name of the state. To sort City objects, we usually want to first sort by city name, then state name (so Austin, Texas would be before Boise, Idaho), but sometimes we want to sort by the state name first, then the city name (in this case, Boise, Idaho would be before Austin, Texas). Of course, we want to use Java generics so that City objects are not mixed up with other types.

The City class implements the Comparable interface, so that sorting methods like Arrays.sort and Collections.sort can be applied to City arrays or City collections. The generic type argument <City> ensures that City objects will only be compared to other City objects.

The CityComparator class implements the Comparator interface so that City objects can be sorted in a different way. Again, the generic type argument <City> ensures that City objects will only be compared to other City objects.

The methods in CityTest.java illustrate the use of these classes, plus an example of how Java generics help detect errors at compile time.

The Iterator and Iterable Interfaces

The Iterator and Iterable interfaces can be used to loop through a sequence of objects. The key methods of the Iterator interface are next and hasNext, so that loops can be written:

while (iterator.hasNext()) {
  SomeType object = iterator.next();
  // process object
}
  
The Iterable interface has a single method iterator that returns an Iterator. This should usually return a new Iterator. An Iterable object can be used to write a for-each loop instead instead of the above while loop:
for (SomeType object : iterable) {
  // process object
}
  
Implementing both interfaces should make use of the generic type parameter, the type of object that is processed in the loops.

The ArrayReverse class implements both interfaces. It can be used to loop through any array of objects in reverse. This follows the typical Iterator implementation, which keeps track of the next element to be returned by the next method. The ArrayReverseTest class shows an example of using ArrayReverse as an Iterator and one example as an Iterable.

Generic Methods

Sometimes it is useful to have individual methods with generics. For example, we might want a method that can return a random element from any type of array of objects.

Integer x = randomElement(integerArray);
String s = randomElement(stringArray);
  
Without generics, we could write a method with an Object[ ] parameter and an Object return type, but then a cast would be needed for the return value.

Another example is implementing a lessThan method that hides the ugly compareTo expression.

// Maybe the second statment looks nicer?
boolean b1 = string1.compareTo(string2) < 0;
boolean b2 = lessThan(string1, string2);

// We want to be able to use the same method to compare other types.
boolean b3 = lessThan(biginteger1, biginteger2);
  
Without generics, we could write a method with Comparable parameters, but the compiler would not ensure that the two parameters were the same type.

The GenericMethodExamples class shows how both of these methods can be implemented to handle any object type. Note how the randomElement method parameterizes the randomInteger and randomString methods. Similarly, the lessThan method parameterizes the lessThanInteger and lessThanString methods. The lessThan method is trickier because it needs to ensure that the Comparable interface is implemented.