Generics Pros and Cons

It all started back in September 2004 with the long awaited Java 5 update based on Java Specification Request 176. You can check it out from JSR page or Oracle’s documentation.
You can find out more about Java Specification Requests from their official site.

Java 5, a.k.a. Java 1.5, is a major feature release! It includes a lot of new and cool stuff that today’s developers take for granted, yet use all the time without  even knowing it. To name the most famous ones:

  • Generics
  • @Annotation support
  • var-args,
  • for each loop,
  • autoboxing/unboxing
  • Enum types
  • java.util.concurrent package,
  • static imports.

However Generics was the biggest and ironically most controversy change. The Java community is still split whether Generics are useful enough to keep around. So…

Why were Generics introduced

“This long-await

ed enhancement to the type system allows a type or method to operate on objects of various types while providing compile-time type safety. It adds compile-time type safety to the Collections Framework and eliminates the drudgery of casting”
– New Features and Enhancements J2SE 5.0, Oracle Documentation

Generics are meant to prevent Run-Time class cast exceptions by early detecting them during Compilation-Time and thus increase type safety when working with Collections.

Before Java 5:

		// list can hold any Оbject
		List list = new ArrayList();

		// String is Object
		list.add( "text" ); 

		// Integer is also Object
		list.add( new Integer( 123 ) );

		// Needs cast, but perfectly legal
		String text = (String) list.get( 0 ); 

		String moreText = (String) list.get( 1 ); // Run-Time:ClassCastException: Integer cannot be cast to String

In this example, Generics will help not only eliminate the Run-Time:ClassCastException, but It will also save you the trouble of casting to String.

After Java 5:

		// compiler will make sure the "list" reference can insert only Strings into its collection
		List<String> list = new ArrayList<String>(); 

		// Perfectly legal
		list.add( "text" );

		list.add( new Integer( 123 ) ); // Compile-Time:The method add(String) ... is not applicable for ... (Integer)

Checking for type compatibility during Compile-Time allows detecting and fixing of class cast exceptions early on, instead of having to find them during Run-Time. So Generics not only help with Type-Safety and casting, but they also save time and embarrassment by finding problems during Compilation-Time.

Generics also help make code reusable. In the previous example, the ArrayList reference “list” was declared with generic type <String>. But String is just one type that can be replaced with any Class or Interface (no primitives). You can have, for example:

		List<Integer> list = new ArrayList<Integer>()

or

		List<MyCustomClass> list = new ArrayList<MyCustomClass>()

or even

		List<ArrayList<MyCustomClass>> list = new ArrayList<ArrayList<MyCustomClass>>()

But most importantly, you can have this:

		List<T> list = new ArrayList<T>()

where “T” is a type declared on class level. This means that any class or interface that declares generic type “T” can use it as a type for field, method param, return type, etc. Thanks to Generics one method/class can be reused with different types.

Then, with so many benefits…

Why people have mixed feeling about them

That one is easy – because of Type-Erasure.

“Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming. To implement generics, the Java compiler applies type erasure to:

  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
  • Insert type casts if necessary to preserve type safety.
  • Generate bridge methods to preserve polymorphism in extended generic types.”

– New Features and Enhancements J2SE 5.0, Oracle Documentation

Type-Erasure is something that Generics apply internally. It basically takes all Generics declarations and replaces them with appropriate types.

For bounded types:

public class Node<T> extends Comparable<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

The Java compiler replaces the bounded type parameter “T” with the first bound class, Comparable:

public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

For unbounded types:

public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) }
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

Because the type parameter “T” is unbounded, the Java compiler replaces it with Object:

public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

You wont see this on your IDE, but that is what happens behind the scenes. And that is how the final Byte-Code will look like. So what you write is just syntactic sugar. The real result is compiled underneath.

The negative effect is that, in the case of unbounded types, type information is getting overridden and not available at Run-Time. This may not be a big issue for all developers, but it is for those how rely on Java Reflection. There are also lots frameworks that use reflection to proxy objects and wrap their methods like Hibernate.

Reflection is also used to inspect compiled classes to determine methods and fields metadata like return types and parameter types. Here is an example class that does that: MethodParameterSpy.

And since the actual types are getting replaced with Object, there is no way of telling what specific type are needed during Run-Time.

Okayyy…then why use Type-Erasure in the first place? Because of backwards compatibility. Java API designers want to allow non-Generics code to still work after Java 5 without creating a maintenance nightmare for everybody.

Still not 100% TypeSafe

Yet another downside of Generics is the they don’t provide full protection. If one is no careful Class Cast Exceptions may still occur. Here is how:

		// create a Vector that will allow only String
		Vector<String> strings = new Vector<String>();

		// perfectly legal
		strings.add( "a string" );

		// cast the Vector to a generic Vector
		// by default, generic vector allows Object
		Vector objects = strings;

		// insert an Object into the Vector
		objects.add( new Object() );

		// fetch an Object from the Vector of strings
		Object anObject = strings.get( 0 );

		// fetch a String from the Vector of strings
		String aString = strings.get( 0 );

The funny thing is that the above code will actually compile successfully giving the developer a false sense of security that wouldn’t last long, because the last line will fail at Run-Time producing: java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String.

Why didn’t the Compiler red-lined this? Because, from Compiler’s point of view, all was well. No attempts to insert a non-String object were made using the parameterized reference “strings”. Line 5 is perfectly legal, because it adds a String.

The problem is on line 9. A new, raw reference called “objects” has been created for the Vector. And then that reference is used to litter the Vector by adding Object to it on line 12.

The actual error is thrown on line 18, because the Object inserted at line 12 cannot be cast to String.

To visualize this:

comparison

 

The Compiler is actually aware of this situation and shows warring on line 9: Vector is a raw type. References to generic type Vector<E> should be parameterized.
But compile warnings are not as efficient as compiler errors when it comes to drawing developer’s attention. And to make matters even worst, warnings can be ignored by annotating them with @SuppressWarnings(“rawtypes”).

So if I decide to ignore one of the many compiler warnings, that may not be even there, I may wrongly assume that some collection reference, not created by me, but one that I intend to use, is type-safe just because I inspected the place of its creation.

		Vector<String> strings = new Vector<String>();

Unknowing for me though somebody may have already have pushed different types of objects to it using raw reference to that collection.

		Vector objects = strings;

Class Cast Exception with Generics will only be a matter of time. Thank God for unit testing!

Final Summation

I personally think that almost perfect Type-Safety + reusability + the ability to early discover potential cast exceptions + backwards compatibility trumps the inconvenience introduced by Type-Erasure.

Links and Resources

Java Generics Tutorial by Oracle
Erasure of Generic Types by Oracle
Java version history, Java 5 by Wikipedia
Java Specification Requests, 176 official page
The Reflection API by Oracle
Java generics and type erasure by Stephen Morley

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s