Saturday, July 3, 2010

Interesting change to method signature erasure rules in Java 7

I found an interesting "bug" that has been fixed in Java 7 compiler. I say "bug" because some may have considered the old behavior to be a nice feature. Consider the following class:
public class ListPrinter {

  public static String getExample(List<String> list) { 
    return list.get(0); // return first
  }

  public static Integer getExample(List<Integer> list) { 
    return list.get(list.size() - 1);  // return last
  }

  public static void main(String[] args) {
    System.out.println(getExample(Arrays.asList("1", "2", "3")));
    System.out.println(getExample(Arrays.asList(1, 2, 3)));
  }
}
In Java 5 and 6, this compiles fine. We have 2 overloaded methods to get an example element from a list. For Lists of String, the first element is returned. For Lists of Integer, the last element is returned. Running the code prints the expected output: 1 3 Obviously, the compiler did the right thing, so what's the problem? Let's look at what happens after type erasure. Running javap on the above class compiled with JDK 6, yields:
public class ListPrinter extends java.lang.Object{
    public ListPrinter();
    public static java.lang.String getExample(java.util.List);
    public static java.lang.Integer getExample(java.util.List);
    public static void main(java.lang.String[]);
}
That's interesting, we've got 2 methods with the same signature getExample(java.util.List) but different return types. The discussion section for 8.4.8.3 of the Java language spec states that "...methods declared in the same class with the same name must have different erasures." The first part is completely clear, but the last part requires some thought. Does different erasure mean: A) different arguments after erasure B) different arguments and return type after erasure In Java 7, the answer is A. But in Java 5 and 6, the answer was B. By strict interpretation, Java 7 got it right. Method overloading requires changing the number and/or types of method arguments. Method signatures are not allowed to differ only by return type. As proof, let's perform the type erasure on the source code:
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

public class ListPrinter {

  public static String getExample(List list) { 
    return (String) list.get(0); // return first
  }

  public static Integer getExample(List list) { 
    return (Integer) list.get(list.size() - 1);  // return last
  }

  public static void main(String[] args) {
    System.out.println(getExample(Arrays.asList("1", "2", "3")));
    System.out.println(getExample(Arrays.asList(1, 2, 3)));
  }

}
Compiling this version of the code in JDK 6 generates the following error:
ListPrinter.java:11: getExample(java.util.List) is already defined in ListPrinter
The compiler is telling us that we cannot have more one than method with the signature getExample(java.util.List). Notice that this is the exact signature that JDK 6 compiler generated twice in the original example. The compiler let us cheat. In Java 7, the original example fails to compile with the error:
ListPrinter.java:11: name clash: getExample(List) and getExample(List) have the same erasure
Thank goodness Java finally fixed that bug. But wait...it was kind of cool that JDK 6 let us do that. Is this a bug or a feature? The compiler figured it out and did the right thing so again I ask, "what's the problem"? The problem is erasure. And it's a problem that Java will likely always be stuck with. I guess it's time to start using Scala. But wait, Scala has type erasure too. Now, here's the million dollar question. What does Scala do in this situation? Here's the equivalient code written in Scala:
object ListPrinter extends Application {

  def getExample(list: List[String]):String = list.head

  def getExample(list: List[Int]):Int = list.last

  override def main(args: Array[String])
  {
    println(getExample(List("1", "2", "3")))
    println(getExample(List(1, 2, 3)))
  }

}
Scala inherits the same rules for overloading with type erasure from Java. But which interpretation of this rule does it use? As an incentive to get people to try Scala, I'm going to let the reader answer this question for themselves by compiling the code. The result may surprise you.