The following code throws a ClassCastException
at runtime, and the line public String foo() { return "bar"; }
generates a warning "found 'java.lang.String', required 'T'". I understand the ClassCastException
(the interface method is called with T
equal to Integer
yet foo
returns a String
) and I understand the warning (it's trying to warn us about exactly this problem). But what I don't understand is why the program compiles at all. Why is a method returning a String
allowed to override a method returning a T
?
public class Main {
interface MyInterface {
<T> T foo();
}
static class MyClass implements MyInterface {
@Override
public String foo() { return "bar"; }
}
public static void main(String[] args) {
int a = ((MyInterface) new MyClass()).<Integer>foo();
}
}
When naively declaring <T> T foo();
the compiler will try to infer the result type of foo
from the variable where it will be assigned. This is the reason why this compiles. It can be easily tested:
interface MyInterface {
<T> T foo();
}
class MyClass implements MyInterface {
@Override
public String foo() { return "bar"; }
}
public class Main {
public static void main(String[] args) {
MyInterface myInterface = new MyClass();
//the result of foo will be String
String bar = myInterface.foo();
System.out.println(bar); //prints "bar"
try {
//the result of foo at compile time will be Integer
Integer fail = myInterface.foo();
System.out.println(fail); //won't be executed
} catch (ClassCastException e) {
//for test purposes only. Exceptions should be managed better
System.out.println(e.getMessage()); //prints "java.lang.String cannot be cast to java.lang.Integer"
}
}
}
And the result at compile time cannot be Object
. If it were object, then you would have to add a manual type cast, which is not the case.
In short, declaring a method like that is useless and can only bring confusion and chaos to the programmers.
This method declaration becomes useful in one of these cases:
When declaring the generic <T>
at top level of the interface/class:
interface MyInterface<T> {
T foo();
}
class MyClass implements MyInterface<String> {
@Override
//can only return String here. Compiler can check this
public String foo() { return "bar"; }
}
When passing Class<T>
as argument, which enables the compiler to infer the result type and raise a proper compiler error when this condition is not met:
interface MyInterface {
<T> T foo(Class<T> clazz);
}
class MyClass implements MyInterface {
@Override
public <T> T foo(Class<T> clazz) {
try {
return clazz.newInstance();
} catch (InstantiationException e) {
e.printStackTrace(System.out);
} catch (IllegalAccessException e) {
e.printStackTrace(System.out);
}
return null;
}
}
public class Main {
public static void main(String[] args) {
MyInterface myInterface = new MyClass();
//uncomment line below to see the compiler error
//Integer bar = myInterface.foo(String.class);
//line below compiles and runs with no problem
String bar = myInterface.foo(String.class);
System.out.println(bar);
}
}
This is a surprisingly deep question. The Java Language Specification writes:
An instance method mC
declared in or inherited by class C
, overrides from C
another method mA
declared in class A
, iff all of the following are true:
mC
is a subsignature (§8.4.2) of the signature of mA
.and:
Two methods or constructors,
M
andN
, 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 ofN
to the the type parameters ofM
, the same formal parameter types.
This is clearly not true in our case, as MyInterface.foo
declares a type parameter, but MyClass.foo
does not.
The signature of a method
m1
is a subsignature of the signature of a methodm2
if either:
m2
has the same signature asm1
, or- the signature of
m1
is the same as the erasure (§4.6) of the signature ofm2
.
The spec explains the need for that second condition as follows:
The notion of subsignature is designed to express a relationship between two methods whose signatures are not identical, but in which one may override the other. Specifically, it allows a method whose signature does not use generic types to override any generified version of that method. This is important so that library designers may freely generify methods independently of clients that define subclasses or subinterfaces of the library.
and indeed, that second condition is met in our case, as MyClass.foo
has the signature foo()
, which also is the erasure of the signature of MyInterface.foo
.
That leaves the matter of the different return type. The spec writes:
If a method declaration
d1
with return typeR1
overrides or hides the declaration of another methodd2
with return typeR2
, thend1
must be return-type-substitutable (§8.4.5) ford2
, or a compile-time error occurs.
and:
A method declaration d1 with return type R1 is return-type-substitutable for another method d2 with return type R2 iff any of the following is true:
...
If R1 is a reference type then one of the following is true:
R1, adapted to the type parameters of d2 (§8.4.4), is a subtype of R2.
R1 can be converted to a subtype of R2 by unchecked conversion (§5.1.9).
d1 does not have the same signature as d2 (§8.4.2), and R1 = |R2|.
In our case, R1 = String and R2 = T. The first condition is therefore false, as String is not a subtype of T. However, String may be converted to T by unchecked conversion, making the second condition true.
The spec explains the need for the second and third conditions as follows:
An unchecked conversion is allowed in the definition, despite being unsound, as a special allowance to allow smooth migration from non-generic to generic code. If an unchecked conversion is used to determine that R1 is return-type-substitutable for R2, then R1 is necessarily not a subtype of R2 and the rules for overriding (§8.4.8.3, §9.4.1) will require a compile-time unchecked warning.
That is, your code is accepted by the compiler because you inadvertently use two compiler features introduced to ease transition to generics by permitting a step by step generification of existing code. These features open a loop hole in the compile time type system, which may cause heap pollution and weird ClassCastExceptions in lines that may not even feature a cast in the source code. To alert you to this danger, the compiler is required to emit an unchecked warning. These features should therefore only be used for their intended purpose (compatiblity with non-generic legacy code), and otherwise avoided.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With