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.

3 comments:

  1. While it appears that "fixing" the "bug" was the "correct" choice, it's unfortunate at a more general level that javac won't allow methods that differ only by return type, although the JVM will allow this. The fact that generics provided a loophole for this has been handy on more than one occasion for refactoring a method signature while preserving binary compatibility.

    ReplyDelete
  2. Looks like Kohsuke has been bit by the compiler's refusal to compile methods whose signature differs only in return type, and has bit back: http://kohsuke.org/2010/08/07/potd-bridge-method-injector/

    ReplyDelete
  3. Useful information shared. Shree Tyres is the best michelin tyres dealers in pune that provides customer safety and satisfaction regarding tyres. For more detail visit our website or contact 8055678063.

    ReplyDelete