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:
Just like generic classes, all generic method declarations have a type parameter section delimited by angle brackets (< and >) -- but unlike generic classes, this type parameter precedes the method's return type ( < E > in the next example).
Just like generic classes, Each type parameter section can contain one or more type parameters separated by commas.
The type parameters can be used to declare the return type and act as placeholders for the types of the arguments passed to the generic method, which are known as actual type arguments.
A generic method's body is declared like that of any other method. Note that, just like with generic classes, type parameters for generic methods can represent only reference types, not primitive types (like int, double and char).
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