Today I stumbled upon some working Java code I wouldn\'t even have expected to compile. Reduced to its bare minimum, it looks like this:
import java.util.List;
While the first example does compile, it will give an unchecked conversion warning:
// Type safety: The return type List<?> for foo() from the type C needs
// unchecked conversion to conform to List<String>
public List<?> foo()
{
return null;
}
What's happening here is that by declaring type parameters, A.foo()
and B.foo()
are generic methods. Then, the overriding C.foo()
omits that type parameter. This is similar to using a raw type, essentially "opting out" of generic type checking for that method signature. That causes the compiler to use the inherited methods' erasures instead: List<String> foo()
and List<Integer> foo()
both become List foo()
, which can therefore be implemented by C.foo()
.
You can see that by keeping the type parameter in the C.foo()
declaration, there will be the expected compiler error instead:
// The return type is incompatible with A.foo()
public <T> List<?> foo()
{
return null;
}
Likewise, if either of the interface methods don't declare a type parameter, then omitting a type parameter from the override fails to "opt out" of generic type checking for that method, and the return type List<?>
remains incompatible.
This behavior is covered in JLS §8.4.2:
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.
Angelika Langer's generics FAQ expands on this behavior in her section Can a non-generic method override a generic one?:
Now, let us explore an example where non-generic subtype methods override generic supertype methods. Non-generic subtype methods are considered overriding versions of the generic supertype methods if the signatures' erasures are identical.
Example (of non-generic subtype methods overriding generic supertype methods):
class Super { public <T> void set( T arg) { ... } public <T> T get() { ... } } class Sub extends Super { public void set( Object arg) { ... } // overrides public Object get() { ... } // overrides with unchecked warning }
warning: get() in Sub overrides <T>get() in Super; return type requires unchecked conversion found : Object required: T public Object get() {
Here the subtype methods have signatures, namely
set(Object)
andget()
, that are identical to the erasures of the supertype methods. These type-erased signatures are considered override-equivalent.There is one blemish in the case of the
get
method: we receive an unchecked warning because the return types are not really compatible. The return type of the subtype methodget
isObject
, the return type of the supertype method get is an unbounded type parameter. The subtype method's return type is neither identical to the supertype method's return type nor is it a subtype thereof; in both situations the compiler would happily accept the return types as compatible. Instead, the subtype method's return typeObject
is convertible to the supertype method's return type by means of an unchecked conversion. An unchecked warning indicates that a type check is necessary that neither the compiler nor the virtual machine can perform. In other words, the unchecked operation is not type-safe. In case of the convertible return types someone would have to make sure that the subtype method's return value is type-compatible to the supertype method's return type, but nobody except the programmer can ensure this.