为什么对隐藏的静态方法强制执行返回类型协方差?

Jai*_*zel 12 java jls

由于in 的String返回类型,此代码将无法编译.staticMethodChild

class Parent {
    static void staticMethod() {    
    }
}

class Child extends Parent {
    static String staticMethod() {
        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

我知道§8.4.8.3中的JLS 8,"覆盖和隐藏的要求"说:

如果具有返回类型R1的方法声明d1覆盖或隐藏具有返回类型R2的另一个方法d2的声明,则对于d2,d1必须是return-type-substitutable(第8.4.5节),否则会发生编译时错误.

我的问题是在静态方法的特定情况下进行编译时检查的动机是什么,这个例子说明在编译期间未能进行此验证会产生任何问题是理想的.

Zho*_*gYu 12

这是Java中最古怪的东西之一.假设我们有以下3个班级

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

public class B extends A
{
}

public class C
{
    static Object x = B.foo();    
}
Run Code Online (Sandbox Code Playgroud)

假设所有3个类都来自不同的供应商,具有不同的发布时间表.

在编译时C,编译器知道该方法B.foo()实际来自A,签名是foo()->Number.但是,调用的生成字节代码不引用A; 相反,它引用方法B.foo()->Number.请注意,返回类型是方法引用的一部分.

当JVM执行这段代码,它首先查找方法foo()->NumberB; 当找不到该方法时,A搜索直接超类,依此类推.A.foo()找到并执行.

现在魔术开始了--B的供应商发布了新版本的B,其"覆盖" A.foo

public class B extends A
{
    public static Number foo(){ return 0.2f; }
}
Run Code Online (Sandbox Code Playgroud)

我们从B获得了新的二进制文件,并再次运行我们的应用程序.(注意,C二进制保持不变;它没有针对新的重新编译B.)Tada!- C.x现在0.2f正在运行!! 因为JVM foo()->NumberB这个时候搜索结束.

这个神奇的功能为静态方法增添了一定程度的活力.但老实说,谁需要这个功能呢?可能没有人.它只会产生混淆,他们希望能够将其删除.

请注意,搜索方式仅适用于父链的单链 - 这就是为什么当Java8在接口中引入静态方法时,他们必须确定这些静态方法不是由子类继承的.

让我们再深入这个兔子洞吧.假设B发布了另一个版本,带有"协变返回类型"

public class B extends A
{
    public static Integer foo(){ return 42; }
}
Run Code Online (Sandbox Code Playgroud)

A据B知道,这可以很好地反对.Java允许它,因为返回类型是"协变"; 这个功能相对较新; 以前,"重写"静态方法必须具有相同的返回类型.

C.x这次会是什么时候?它是0.1f!因为JVM没有找到foo()->NumberB; 它的发现A.JVM考虑()->Number并且()->Integer作为两种不同的方法,可能支持在JVM上运行的一些非Java语言.

如果C针对这个最新的重新编译B,C的二进制文件将参考B.foo()->Integer; 然后在运行时,C.x将是42.

现在,B的供应商在听到所有投诉后,决定foo从B中删除,因为"覆盖"静态方法是非常危险的.我们从B获得新的二进制文件,并再次运行C(不重新编译C) - 繁荣,运行时错误,因为B.foo()->Integer在B或A中找不到.

这整个混乱表明,允许静态方法具有"协变返回类型"是一种设计疏忽,这实际上仅用于实例方法.

更新 - 在某些用例中,此功能可能很有吸引力,例如,静态工厂方法 - A.of(..)返回A,而B.of(..)返回更具体B.API设计人员必须小心并理解潜在的危险用法.如果AB来自同一作者,并且它们不能被用户子类化,则此设计非常安全.