Sunday, August 15, 2010

Mixing Generic & Non-generic code in Java

Can you spot the error in the following code:

public class MixingGenerics{

public static void main(String[] args){
List<Integer> arr = new ArrayList<Integer>();
arr.add("String 1");
arr.add("String 2");

// add something more
addToList(arr);
}

public static void addToList(List arr){
arr.add(new Integer(1));
}
}

I’m sure you are able to point out the mistake that we are adding an Integer to an ArrayList of Strings. That’s definitely not done. But can you tell without compiling this code that we get an error at compile-time or at run-time ?

Unfortunately and even more surprisingly, this code doesn’t give any compilation error or runtime exception. Everything works fine and an Integer is added into your ArrayList (of Strings) !! Yes, you do get a compiler warning that says something about Type Safety of your code. We will get back to this point later on.

But first examine how is possible that you can actually store an Integer in a String Arraylist? Actually, it is an unfortunate outcome of mixing Generic & Non-generic code. The reason why we get no error is due to a property of Generics called Type-Erasure. We’ll get to it soon but first lets see when the previous code will throw an Error/Exception. This code will explode as soon as you would try and iterate the ArrayList of Strings, in which you have put an Integer unknowingly as shown below:

public class MixingGenerics{

public static void main(String[] args){
List<Integer> arr = new ArrayList<Integer>();
arr.add("String 1");
arr.add("String 2");

// add something more
addToList(arr);

// print the list
printMyList(arr);
}

public static void addToList(List arr){
arr.add(new Integer(1));
}

public static void printMyList(List<String> arr){
for(String s:arr){
System.out.println("String : "+s);
}
}
}

So we get a runtime exception now on iterating the list. It prints the first two strings and then throws ClassCastException as we are trying to cast an Integer into a String s. That’s okay but why does it allows you to add an Integer into List of Strings.

Type-Erasure

Generic code in Java exhibit a very important property: Type-erasure. What it means is that the so-called type-safe collection of yours (the generic collection here: ArrayList<String>) loses its type information at run-time and acts as if its just a List (like pre-Java 5 lists, the non-generic list). This has been made such so that we can integrate pre-Java 5 (the non-generic code) with newly written Generic code (Java 5 or later), but then while mixing them, you must be careful. Very careful indeed, else you could happen to write a code that will explode unpredictably.

So the answer to our question that why it allows an Integer to a list of Strings is because at runtime we only have a list, just like we had a list in pre-Java 5 days. Without generics, we had to take care while taking object out of the list and casting them but with Java 5 and above code, that casting is implicitly inserted by the compiler. So when we say

String s = arr.get(0) gets converted into String s = (String) arr.get(0)

Doing exactly the same with our list, it gives an error when we try and cast an Integer into a String and hence the error: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

Apart from giving the error, the compiler does his best and warns you when you are trying to mix Generic code with Non-Generic Code. If you are very sure about your code and do not want any compiler warning, then you can do by using following Annotation (valid with Java5+ code)

@SuppressWarnings("unchecked")
public static void addToList(List arr){
arr.add(new Integer(1));
}

This lets the compiler know that you are pretty sure about the code that you have written and do not give me any warnings with this method. Generally, it is not recommended to use such Annotations and if you really need to use one, you must do proper documentation of your code.

Any comments and corrections are welcome :-)

No comments: