Tuesday 12 July 2011

Handling raw type and type safety warnings when using legacy code

The addition of generics to Java 5 enabled type checking at compile time. This assists in preventing a ClassCastException at runtime but to maintain backward compatibility generics also came with Type Erasure:
Type erasure exists so that new code may continue to interface with legacy code. Using a raw type for any other reason is considered bad programming practice and should be avoided whenever possible.

When mixing legacy code with generic code, you may encounter warning messages similar to the following:

Note: WarningDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Unfortunately for projects with large legacy code bases or dependencies on legacy third party libraries the raw type warnings can be a major irritation if you prefer not to have false-positive warnings. The solution is to add the @SuppressWarnings("rawtypes") annotation which works as expected, telling the compiler to ignore the raw type issue. The problem with this approach (as discussed elsewhere) is that your code ends up having an annotation for each call to the legacy code which in turn reduces readability. Adding @SuppressWarnings({ "unchecked", "rawtypes" }) at the class level will also suppress all the warnings but may unintentionally mask instances that should be flagged as warnings. An alternative approach is provided below.

Lets assume that you have a legacy code method (which you are not able to change) as defined below:
/**
     * Retrieve the list of values
     * @return A vector of string values
     */
    public static Vector getValues();

In your code you would like to use generics to process the returned vector as below:
public static void main(String[] args) {
        Vector<String> safeVector = getValues();
        for (String value : safeVector) {
            System.out.println(value);
        }
    }

Compiling your code gives the "uses unchecked or unsafe operations" warning. Rather than adding the suppress warnings to every line where getValues() is being called you can instead define a wrapper method returning the typed vector as below:
@SuppressWarnings({ "unchecked", "rawtypes" })
    public static <T> Vector<T> castType(Vector v) {
        return (Vector<T>) v;        
    }

This means your code is changed as follows:
public static void main(String[] args) {
        Vector<String> safeVector = castType(getValues());
        for (String value : safeVector) {
            System.out.println(value);
        }
    }

The warning messages are no longer a problem and the code functions as expected. A few notes on the implementation:

  • The type T is inferred from the Vector<String> declaration, the castType method would work equally well for an integer vector (Vector<Integer>) .
  • The warning is merely being suppressed, the castType method is not actually checking the type of the vector elements. A runtime exception will occur when accessing an element if the vector passed to the method is not actually of the declared type.
  • The castType method should only be used when you are certain of the type of elements contained in the vector (restating the above point).
  • The castType implementation is using vectors to demonstrate the concept but can be extended to other types (or even generalised to use the Collection interface)
  • An argument can be made against the overhead of castType method call instead of a class level @SuppressWarnings annotation, this implementation merely provides an alternative.

For completeness the generalised method would look as follows:
@SuppressWarnings({ "unchecked", "rawtypes" })
    public static <T> Collection<T> castType(Collection v) {
        return (Collection<T>) v;        
    }