It is known that one of the limitations of Java is the inability to overload a method by return type as the return type is not part of the signature of the method.
According to https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.4.2,
“Two methods or constructors, M and N, have the same signature if they have the same name, the same type parameters (if any) (§8.4.4), and, after adapting the formal parameter types of N to the the type parameters of M, the same formal parameter types.”
In other words, a class like this would be rejected by the compiler:
public class MyClass {
public static Integer print(){
System.out.println("hello");
return null;
}
public static String print(){
System.out.println("hello,Integer");
return null;
}
}
javac -cp myclasses/ -g -d myclasses/ src/main/java/fjab/overloading/MyClass.java
src/main/java/fjab/overloading/MyClass.java:13: error: method print() is already defined in class MyClass
public static String print(){
^
1 error
The reasoning for this behaviour is that when calling a method, the return type is not specified as part of the invocation and therefore there is no way for the compiler to know which method to call.
However, when the class is compiled to bytecode, the return type if part of the descriptor of the method and therefore the JVM allows to overload by return type.
The rest of this post will illustrate this behaviour with an example.
Example
Let’s consider these classes
public class MyClass {
public static Integer print(){
System.out.println("hello");
return null;
}
}
public class MyApp {
public static void main(String[] args){
MyClass.print();
}
}
After compiling and running MyApp, everything looks ok.
javac -cp myclasses/ -g -d myclasses/ src/main/java/fjab/overloading/*.java
java -cp myclasses/ fjab.overloading.MyApp
hello
Let’s change the return type of MyClass.print. As the return type is not part of the method signature, this change should not have any effect
public class MyClass {
public static String print(){
System.out.println("hello");
return null;
}
}
After compiling MyClass and running MyApp
javac -cp myclasses/ -g -d myclasses/ src/main/java/fjab/overloading/MyClass.java
java -cp myclasses/ fjab.overloading.MyApp
Exception in thread "main" java.lang.NoSuchMethodError: fjab.overloading.MyClass.print()Ljava/lang/Integer;
at fjab.overloading.MyApp.main(MyApp.java:10)
What??? After all, the return type is taken into consideration by the JVM. As far as the JVM is concerned, the return type is part of the method signature.
So, would it be possible to overload two methods by return type in the JVM? Using the ASM framework, we will try to do so.
At the moment, MyClass.class contains only the constructor and a static method:
javap myclasses/fjab/overloading/MyClass.class
Compiled from "MyClass.java"
public class fjab.overloading.MyClass {
public fjab.overloading.MyClass();
public static java.lang.String print();
}
After running the program to modify the bytecode (https://github.com/fjab76/asm/blob/master/src/main/java/fjab/adapter/overloading/DuplicateMethodAdapter.java), a new static method print() is added with the same signature than the existing one except the return type
javap myclasses/fjab/overloading/MyClass.class
Compiled from "MyClass.java"
public class fjab.overloading.MyClass {
public fjab.overloading.MyClass();
public static java.lang.String print();
public static java.lang.Integer print();
}
This class is completely functional as it can be shown by running MyApp again
java -cp myclasses/ fjab.overloading.MyApp
hello
Examining the classes more in detail, it is clear that both print() methods are identical but for the return type:
javap -v myclasses/fjab/overloading/MyClass.class
public static java.lang.String print();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aconst_null
9: areturn
LineNumberTable:
line 9: 0
line 10: 8
public static java.lang.Integer print();
descriptor: ()Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #34 // String hello,Integer
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aconst_null
9: areturn
Interestingly, Integer print() does not have LineNumberTable as it does not exist in the source file!! LineNumberTable matches the lines in the source file to the bytecode instructions. This information is useful for the debuggers.
This bytecode represents the class at the beginning of this post. What the java compiler would not allow has been accomplished manipulating the bytecode.
As to MyApp, the bytecode indicates that it is invoking the method print() that returns String
javap -v myclasses/fjab/overloading/MyApp.class
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: invokestatic #2 // Method fjab/overloading/MyClass.print:()Ljava/lang/String;
3: pop
4: return
LineNumberTable:
line 10: 0
line 11: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 args [Ljava/lang/String;
MyApp can even be compiled again without any problem, the compiler keeps linking the call MyClass.print() to the method returning String.
javac -g -cp myclasses/ -d myclasses/ src/main/java/fjab/overloading/MyApp.java
java -cp myclasses/ fjab.overloading.MyApp
hello
What if we manipulate the bytecode to add the method that returns Integer before the one returning String? Using https://github.com/fjab76/asm/blob/master/src/main/java/fjab/adapter/overloading/PrependDuplicateMethodAdapter.java it is possible to do so.
javap -v myclasses/fjab/overloading/MyClass.class
public static java.lang.Integer print();
descriptor: ()Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #34 // String hello,Integer
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aconst_null
9: areturn
public static java.lang.String print();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aconst_null
9: areturn
LineNumberTable:
line 9: 0
line 10: 8
In this case, the compiler links the call MyClass.print() to the method returning Integer. So the compiler seems to choose the first suitable method found in the target class.
javac -g -cp myclasses/ -d myclasses/ src/main/java/fjab/overloading/MyApp.java
java -cp myclasses/ fjab.overloading.MyApp
hello,Integer
Technical details
This examples have been run on OS X 10.11.1 using:
java -version
java version “1.8.0_45”
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)
javac -version
javac 1.8.0_45