Generics & Type Parameters

We can use generic classes and type parameters in many, many contexts -- but so that we can develop a concrete example, let us suppose that we are trying to implement a stack to store various objects. Indeed, perhaps we see in our future the need for a stack of Strings for one program, a stack of Files for another -- maybe even a stack of objects of type Card for a poker program.

Without type parameters, we might try to implement separate stack classes for each type, but rewriting and maintaining such code is tedious and error-prone. (Note, we had no other way to do this until Java 1.5!)

We alternatively might try creating stacks where the elements were of Object type, but then the client has to deal with casting, which is error-prone. In particular, run-time errors can occur if types don't match, as shown below:

StackOfObjects s = new StackOfObjects();
Apple a = new Apple();
Orange b = new Orange();
s.push(a);
s.push(b);
a = (Apple) (s.pop()); // <-- this produces a run-time error

With a generic class, we can avoid casting in the client code, and type mismatch errors can be discovered at compile-time instead of run-time, as the below code demonstrates. Of course, as programmers, we always much prefer compile-time errors over run-time errors -- they are far easier to find and fix!

Stack<Apple> s = new Stack<Apple>();
Apple a = new Apple();
Orange b = new Orange();
s.push(a);
s.push(b); // <-- compile-time error
a = s.pop();

As can be seen above, we pass to the class Stack the type of object we require it to contain (i.e., "Apple") in between angle brackets.

To see how to make a generic class that accepts a type parameter, let's take a peek "under the hood" at how the generic Stack class above might have been defined:

public class Stack<Item> implements Iterable<Item>{                                            

	private Item[] items;   // <-- we can use "Item" as a type now!
	private int size;
	
	public Stack() {
		//items = new Item[];  // <-- generic array creation is not allowed :o(
		items = (Item[]) (new Object[1]);  // <-- so we have to settle for this        
		size = 0; 
	}

       /* .... */ 
}

The key piece above that makes the Stack class above generic is the <Item> immediately following the word Stack. When such angle brackets are present after the class name, the type name specified (e.g., Item) will be replaced in the rest of the code that makes up the class with whatever the client specifies (Apple for instance). In the implementation shown, this replacement includes where Item is passed as a type parameter to the Iterable interface in the latter part of the class declaration. There are some exceptions, however. For example, generic array creation is not allowed in Java, so where we might want to do this: Items[] items = new Item[], we have to settle for a statement with a very ugly cast: Items[] items = (Item[]) (new Object[1]);.

In some circumstances, one may wish to require that whatever class the client specifies for Item implements some interface. For example, suppose we wanted to limit our custom Stack class above to supporting only the pushing of items that come from a class that implements the Comparable interface. In this case, we would change the class declaration to look like this:

public class Stack<Item extends Comparable<Item>> implements Iterable<Item>{ /* .... */ }

One is not limited to a single type parameter in a generic class either. We just separate the types by commas inside the angle brackets. As an example, suppose a class named BST needed the client to specify two types, to be named Key (which must implement the Comparable interface) and Value inside the class. Here's what the class declaration would look like:

public class BST<Key extends Comparable<Key>, Value> { /* ... */ }

When we require a type parameter to implement a particular interface or to extend a particular class (as is the case with both the Item and Key type parameters above), we say the type parameter is a bounded type parameter. Curiously, regardless of whether we are bounding our type parameter with a class or an interface, we always use the extends keyword -- never the word implements.

The preceding examples showed some type parameters with a single bound, but a type parameter can have multiple bounds too! Here's the syntax:

<T extends B1 & B2 & B3>
A type parameter with multiple bounds is considered a subtype of all the types listed in the bound. In other words, if one of the types listed is a class, then the type parameter must be a subclass of that class; if one of the types listed is an interface, then the type parameter must implement that interface.

Importantly, if one of the types listed in the bound is a class, it must be specified first. For example:

class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ }

class E <T extends B & A & C> { /* ... */ }   // <-- compile-time error, the type A
                                              //     needs to come first as it is a class

One can make generic methods too!

The following are the rules of the road when defining generic methods:

The following example shows how one can use a single generic method to print the contents of any array, regardless of the type of objects the array contains:

public class GenericMethodTest {

   public static <E> void printArray( E[] inputArray ) {
      for (E element : inputArray) {
         System.out.printf("%s ", element);
      }
      System.out.println();
   }

   public static void main(String args[]) {
      Integer[] intArray = { 1, 2, 3, 4, 5 };
      Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
      Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };

      printArray(intArray);      // passed an Integer array
      printArray(doubleArray);   // passed a Double array
      printArray(charArray);     // passed a Character array
   }
}

Here's the output of the above:

1 2 3 4 5 
1.1 2.2 3.3 4.4 
H E L L O