Java bytecode is strongly typed and verified, which implies that the caller’s code must be compatible to what the invoked method will return. So even if the caller’s method reference did not contain the expected return type, the code did still contain implicit assumptions, e.g. trying to do long
arithmetic with the result indicates that the method is expected to return long
rather than Object
or void
.
Having the method references indicating the expected return type simplifies verification and makes the entire process more efficient. You can verify the correctness of a method’s code using expected method signatures without performing an actual linkage. When a method invocation instruction is finally linked, there is no need to verify the executable code, only the signatures have to match.
That’s why Java byte code had been designed that way, even if Java source code could not define different return types, at least in the earlier versions. Starting with Java 5, the rules are not that strict anymore.
Consider the following interface:
interface StringFunction<R> {
R apply(String input);
}
Due to type erasure, it will have a method Object apply(String input)
on the byte code level.
Now consider the following implementation class:
class Length implements StringFunction<Integer> {
public Integer apply(String input) {
return input.length();
}
}
Not only is declaring a more specific return type allowed, it is actually required by the Java language as according to the Generic type system, it inherits an abstract method Integer apply(String)
from StringFunction<Integer>
.
On the byte code level, it will have the actual implementation method Integer apply(String)
as well as a bridge method Object apply(String input)
formally fulfilling the contract of the interface
on the byte code level and delegating to the actual implementation method.
Since Generics effectively allow a narrowing of the return type, there was no reason to deny it for non-Generic methods, hence, Java allows so called covariant return types since Java 5 as well:
class Base {
Object getValue() {
return null;
}
}
class Sub extends Base {
@Override String getValue() {
return "now a string";
}
}
so it is possible to produce classes having multiple methods with the same parameter types but different return types, though not by overloading.
These cases could be handled alternatively, e.g. by defining that methods are distinguished by parameter types only, and their return type must be the same or more specific, to be compatible with covariant return types, but that would imply that a JVM had to eagerly resolve all return types when building the method table of a class, to verify whether the type really is more specific. And still, it would require the return type to be encoded to establish a proper contract between caller and callee.