Before generic was added to the Java language, there was a class Super
, which was defined as:
class Super {
public Object transform(Object t) { ... }
public Object get() { ... }
}
As a user of this class, I can derive a subclass Sub
from Super
:
class Sub extends Super {
public Object transform(Object t) { ... } // override
// The following cases are also considered as method override.
// public String transform(Object t) { ... }
// public List transform(Object t) { ... }
// and so on.
public Object get() { ... } // override
}
The transform
and get
in Sub
override the transform
and get
in Super
. One day, generic was added to the Java language, the author of the Super
class wanted to re-engineering, he changed Super
to a new form using generic:
class Super {
public <T> T transform(T t) { ... }
public <T> T get() { ... }
}
For backward compatibility, Java allows a non-generic method in subclass to override a generic method in superclass as long as they have the same erasure. The signature of transform
in Sub
and the signature's erasure of transform
in Super
are identical. In this way, regardless of whether it is before or after the Super
's re-engineering, users of Super
do not need to modify the code using Super
.
If Super
is refactored into a generic type, users of Super
actually also do not need to modify any code. However, in this case, the raw type is used in Sub extends Super
.
class Super<T> {
public T transform(T t) { ... }
public T get() { ... }
}
Now define a subclass of Super<T>
-- Sub<T>
:
class Sub<T> extends Super<T> {
public Object transform(Object t) { ... } // override
// The following cases are also considered as method override.
// public String transform(Object t) { ... }
// public List<Integer> transform(Object t) { ... }
// and so on.
}
The transform
in Sub<T>
has the same signature as the signature's erasure of the transform
in Super<T>
. This prompts me to judge: If a get()
method is added to Sub<T>
, it should also override the get()
method in Super<T>
. However, during compilation, I found that not only does it fail to override, but it also results in an incompatible return type
error. Why, in this case, can't the get()
method in Sub<T>
apply the same erasure rule to achieve overriding?
// after adding get() method to Sub<T>
class Sub<T> extends Super<T> {
public Object transform(Object t) { ... } // override
public Object get() { ... } // error: incompatible return type
}
After all, in the following form of code, the get() method in the subclass does override the get() method in the superclass. Why can't this rule be applied in the case where I encountered the error?
class Super {
public <T> T transform(T t) { ... }
public <T> T get() { ... }
}
class Sub extends {
public Object transform(Object t) { ... } // override
public Object get() { ... } // override!
}
The subclass method having a signature that is the erasure of the superclass method is indeed a necessary condition for overriding methods. In the words of the Java Language Specification, if m1
overrides m2
, the signature of m1
must be a subsignature of the signature of m2
.
The signature of a method m1 is a subsignature of the signature of a method m2 if either:
m2 has the same signature as m1, or
the signature of m1 is the same as the erasure (§4.6) of the signature of m2.
The signature of a method includes its name, type parameters, and formal parameter types. Notably, the signature does not include the return type.
However, having a subsignature is not sufficient for overriding. There are additional requirements. In particular, the Object get()
method fails to satisfy the following:
If a method declaration d1 with return type R1 overrides or hides the declaration of another method d2 with return type R2, then d1 must be return-type-substitutable (§8.4.5) for d2, or a compile-time error occurs.
For R1
and R2
that are reference types, fulfilling any of these will make the method declarations return-type-satisfiable:
- 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|.
Object get()
is not return-type-substitutable for T get()
because
Object
is not a subtype of T
Object
cannot be converted to T
using an unchecked conversion. (It is possible to convert from Object
to T
using a narrowing reference conversion that is unchecked, but this is different from the "unchecked conversions" specified in §5.1.9)Object get()
has the same signature as T get()
. Remember that method signatures do not include the return type.Object transform(Object t)
is return-type-substitutable for T transform(T t)
, because they have different signatures, and the erasure of T
is Object
.
Object get()
is return-type-substitutable for <T> T get()
as well, because these have different signatures, and the erasure of T
is Object
. Recall that method signature includes the generic type parameters too. <T> T get()
has one type parameter, and Object get()
has none.