Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is return-type covariance enforced for hidden static methods?

Tags:

java

jls

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.

like image 988
Jaime Hablutzel Avatar asked Jun 16 '15 18:06

Jaime Hablutzel


People also ask

What is the use of covariant return type in Java?

Covariant return type refers to return type of an overriding method. It allows to narrow down return type of an overridden method without any need to cast the type or check the return type. Covariant return type works only for non-primitive return types.

What do you understand by covariant return type?

In object-oriented programming, a covariant return type of a method is one that can be replaced by a "narrower" type when the method is overridden in a subclass. A notable language in which this is a fairly common paradigm is C++.

Do static methods have a return?

it is a static method (static), it does not return any result (return type is void), and. it has a parameter of type array of strings (see Unit 7).

Can we use different return types for methods when overridden?

Yes. It is possible for overridden methods to have different return type .


Video Answer


1 Answers

This is one of the most bizzare things in Java. Say we have the following 3 classes

public class A
{
    public static Number foo(){ return 0.1f; }
}

public class B extends A
{
}

public class C
{
    static Object x = B.foo();    
}

Let's say all 3 classes are from different vendors with different release schedules.

At compile time of C, the compiler knows that method B.foo() is actually from A, and the signature is foo()->Number. However, the generated byte code for the invocation does not reference A; instead, it references method B.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 in B; when the method is not found, the direct super class A 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

public class B extends A
{
    public static Number foo(){ return 0.2f; }
}

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 new B.) Tada! - C.x is now 0.2f at runtime!! Because JVM's searching for foo()->Number ends in B 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"

public class B extends A
{
    public static Integer foo(){ return 42; }
}

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 is 0.1f! Because JVM does not find foo()->Number in B; it's found in A. 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 newest B, C's binary will reference B.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, because B.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(..) returns A, while B.of(..) returns a more specific B. The API designers must be careful and reason about potential dangerous usages. If A and B are from the same author, and they cannot be subclassed by users, this design is quite safe.

like image 182
ZhongYu Avatar answered Sep 29 '22 05:09

ZhongYu