Thursday, June 23, 2011

Reification in Java

"I still get unchecked warnings!" - a sentence I hear from time to time when I am sitting close to people trying to use generics. Every unchecked warning represents the potential for a ClassCastException at run-time. Do your best to eliminate these warnings if possible - which is not always the case. The more experience you acquire with generics, the fewer warnings you’ll get, but don’t expect newly written code that uses generics to compile cleanly.
Why do we get these warnings? What's wrong with generics? In short the answer is: they are not reified. In Java a type is reifiable if the type is completely represented at runtime, that is, if erasure does not remove any useful information.

A type is not reifiable if it is one of the following:
  • A type variable (such as T)
  • A parameterized type with actual parameters (such as List<Number>, ArrayList<String>, or Map<String, Integer>)
  • A parameterized type with a bound (such as List<? extends Number> or Comparable<? super String>)
Generics are implemented using erasure, in which generic type parameters are simply removed at run-time. That doesn't render generics useless, because you get type checking at compile-time based on the generic type parameters, and also because the compiler inserts casts in the code (so that you don't have to) based on the type parameters.


        Map<String, String> countryCodes = new HashMap<String, String>();
        countryCodes.put("US", "United States");
        String country = countryCodes.get("US");

If you compile the source above and then de-compile the generated class file you will see that they are not the same:


        Map countryCodes = new HashMap();
        countryCodes.put("US", "United States");
        String country = (String) countryCodes.get("US");

You should note two things in the code above:
  1. the type parameter "<String, String>" is missing
  2. (String) countryCodes.get("US"); typecast is added
This is done by the java compiler while compiling java source code to byte code. This process is called type erasure.
Generics are implemented using erasure as a response to the design requirement that they support migration compatibility: it should be possible to add generic type parameters to existing classes without breaking source or binary compatibility with existing clients.

While solving one set of problems, erasure adds a set of its own problems.

For a type parameter T, you can't
  • write a class literal T.class.
  • use instanceof to test if an object is of type T
  • create an array of T
  • write class literals for generic types like List<String>.class
  • test if an object is an instanceof List<String>
  • create an array of List<String>
Being unable to create an array of a parameterized type also means that you can get confusing warnings when using varargs methods in combination with generic types. This is because every time you invoke a varargs method, an array is created to hold the varargs parameters. If the element type of this array is not reifiable, you get a warning.


    public static <T> List<T> toList(T... item) {
        List<T> list = new ArrayList<T>();
        for (T element : item) list.add(element);
        return list;
    }

    public static <K> List<K> singleton(K element) {
        return toList(element); // unchecked warning
    }

There is little you can do about these warnings other than to suppress them, and to avoid mixing generics and varargs in your APIs.

No comments:

Post a Comment