Friday, February 25, 2011

Language Features of Java Generics

One of the main features introduced with Java 2SE 5.0 are generics; which provide the means to create general implementations for different types (a class or an Interface). Think about the possibility of creating a single Generic Stack class for every different type you wish to store, without the need to implement the same Stack functionality for the different types over and over again (StackOfIntegers, StackOfString, StackOfDoubles), just because it will hold elements of different types. It would be even nicer if besides the previous feature we could detect type mismatches at compile time; known as compile-time type safety. For example, if a Stack stores only Integers, attempting to push a String on to that Stack should issue a compile-time error. That is exactly what Java Generics is about; generic methods and classes enable programmers to specify with a single method declaration a set of related methods or, with a single class declaration a set of related types respectively. As well as providing compile-time type safety that allows programmers to catch invalid types at compile time.

Motivation of Generics

a) Code Reuse

Overloaded methods are often used to perform similar operations on different types. In the next example we can see three overloaded printArray methods, these methods print the String representation of the elements Integer, Double and Character arrays. It is important to note that only reference types can be used with generics, that’s why we cannot use their respective primitive types (int, double, char).
public static void printArray( Integer[] inputArray ){
for ( Integer element : inputArray )
System.out.printf( "%s ", element );
}

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

public static void printArray( Character[] inputArray ){
for ( Character element : inputArray )
System.out.printf( "%s ", element );
}
As you can see these three overloaded methods perform the exact same logic, the only difference between them is the type each method receives as a parameter and the type the for statement loops through. This example should be a good opportunity to write a generic method; it appears that if we can replace the array's element type in each of the three methods with a single generic type, then we should be able to declare one printArray method that can display the String representations of the elements of any array that contains objects. Let’s see how the new generic method will look:
    public static <T> void printArray( T[] inputArray )
{
// display array elements
for (T element : inputArray)
System.out.print(element + " ");

System.out.println();
} // end method printArray
Method printArray ‘s type parameter section declares type parameter T, as the placeholder for the array element type that printArray will process. T is replaced in the parameter list as the array element type as well it will be replaced in the for statement. These are the exact same locations where the overloaded printArray methods made use of Integer, Double, Character as the array element type.

As you can see writing overloaded methods to perform the same implementation might be in many cases time consuming, error prone and will make our code harder to maintain. By declaring printArray as a generic method we saved nearly 20 lines creating a reusable method that implements the same functionality regardless the type of element we are sending to it.

b) Compile-time type safety

Before Java 2SE 5.0 this kind of generic programming was achieved by means of Object references. For example a generic list was implemented as a collection of Object references. Since Object is the superclass of all classes the list of Object references can hold references to any type of Object. Elements were added to the collection by passing an element reference. Each time it was extracted an object from a collection it was received an Object reference. Before the retrieved element could be effectively used, we were suposed to restore the element’s type information. For this purpose it was required to cast the returned Object reference down to the elements type. Here is an example:
LinkedList list = new LinkedList();
list.add(new Integer(0));
Integer i = (Integer) list.get(0);
It was required to cast the Object reference returned from method get()and promise the compiler that we were going to assign an Integer reference to i . The cast is safe because it is checked at runtime. If we tried a cast to a type different from the extracted element’s actual type, a ClassCastException would be raised, like in the example below:
// fine at compile-time, but fails at runtime with a ClassCastException
String s = (String) list.get(0);
Now, this latter approach is not that effective because we cannot identify possible bugs until runtime (When the application crashes or in best case throws an error). Additionally, the lack of information about the collection’s element type required countless casts in all places where elements are extracted from a collection, making our code harder to read and unsafe. With generics, information about the type elements a collection might contain is provided to the compiler. Instead of treating every collection as a collection of Object references, we will distinguish between collections of references to integers and collections of references to Strings and so on. A collection type would be a parameterized (or generic) type that has a type parameter, which would specify the element type. With a generic list, the previous example would look like this:
LinkedList <Integer> list = new LinkedList <Integer> ();
list.add(new Integer(0));
Integer i = list.get(0);
With this approach the get()method of a generic list returns a reference to an object of a specified type, in this case Integer, that means the cast from Object to Integer is not required because the compiler knows the method will return an Integer from the list. As well, if the extracted element was of a different type would now be caught at compile time already, rather than at runtime. The example below would simply not compile:
String s = list.get(0); // compile-time error
This way, Java Generics provide programmers with a powerful capability for software reuse with compile-time type safety.

No comments:

Post a Comment