This code won't compile because of the String
return type of the staticMethod
in Child
.
class Parent {
static void staticMethod() {
}
}
class Child extends Parent {
static String staticMethod() {
return null;
}
}
I know that JLS 8 in §8.4.8.3, "Requirements in Overriding and Hiding" says:
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.
My question is what has been the motivation for this compile-time checking in the specific case of static methods, an example ilustrating that the failure to do this verification during compilation would produce any problems would be ideal.
This is one of the most bizzare things in Java. Say we have the following 3 classes
Let's say all 3 classes are from different vendors with different release schedules.
At compile time of
C
, the compiler knows that methodB.foo()
is actually fromA
, and the signature isfoo()->Number
. However, the generated byte code for the invocation does not referenceA
; instead, it references methodB.foo()->Number
. Notice that the return type is part of the method reference.When JVM executes this code, it first looks for method
foo()->Number
inB
; when the method is not found, the direct super classA
is searched, and so forth.A.foo()
is found and executed.Now the magic starts - B's vendor releases a new version of B, which “overrides”
A.foo
We got the new binary from B, and run our app again. (Note that
C
's binary stays the same; it has not been recompiled against the newB
.) Tada! -C.x
is now0.2f
at runtime!! Because JVM's searching forfoo()->Number
ends inB
this time.This magical feature adds some degree of dynamism for static methods. But who needs this feature, honestly? Probably nobody. It creates nothing but confusions, and they wish they could remove it.
Notice that the way of searching only works for single chain of parents - that's why when Java8 introduced static methods in interfaces, they had to decide that these static methods are not inherited by subtypes.
Let's go down this rabbit hole a little further. Suppose B releases yet another version, with "covariant return type"
This compiles fine against
A
, as far as B knows. Java allows it because the return type is "covariant"; this feature is relatively new; previously, "overriding" static method must have the identical return type.And what would
C.x
be this time? It is0.1f
! Because JVM does not findfoo()->Number
inB
; it's found inA
. JVM considers()->Number
and()->Integer
as 2 distinct methods, probably to support some non-Java languages that runs on JVM.If
C
is recompiled against this newestB
, C's binary will referenceB.foo()->Integer
; then at runtime,C.x
will be 42.Now, B's vendor, after hearing all the complaints, decides to remove
foo
from B, because it is so dangerous to "override" static methods. We get the new binary from B, and run C again (without recompiling C) - boom, runtime error, becauseB.foo()->Integer
is not found in B or in A.This whole mess indicates that it was a design oversight to have allowed static methods to have "covariant return type", which is really only intended for instance methods.
UPDATE - this feature might be charming in some use cases, for example, static factory methods -
A.of(..)
returnsA
, whileB.of(..)
returns a more specificB
. The API designers must be careful and reason about potential dangerous usages. IfA
andB
are from the same author, and they cannot be subclassed by users, this design is quite safe.