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

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

由于 ChildstaticMethodString return 类型,此代码无法编译。

class Parent {
    static void staticMethod() {    
    }
}

class Child extends Parent {
    static String staticMethod() {
        return null;
    }
}

我知道 §8.4.8.3 中的 JLS 8,"Requirements in Overriding and Hiding" 说:

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.

我的问题是在静态方法的特定情况下进行这种编译时检查的动机是什么,一个示例说明如果在编译过程中未能进行此验证会产生任何问题将是理想的。

String 不是 void 的子类型。现在进入实际问题:

此限制的症结在于 static 方法确实在 Java 中继承但不能被覆盖。如果我们必须找到编译时检查 return 类型的 static 方法的动机,真正要问的问题是 为什么静态方法在 Java 中继承但不能被覆盖?

答案很简单。

static 方法不能是 overriden,因为它们属于 class 而不是 instance。如果你想知道这背后的动机,你可以看看 this 已经有一些答案的问题。 static 方法是允许继承的,因为在无数情况下,subclass 会想重用 static 方法而不必重复相同的代码。考虑一个计算 class :

的实例数的简单示例
class Parent {
   private static int instanceCount = 0;

   public Parent() {
       ++instanceCount;
   }

   public static int staticMethod() { 
       return instanceCount;   
   }

   //other non-static/static methods
}

class Child extends Parent {
    //.. other static/non-static methods
}

Parent 知道如何计算自己创建的 instances 的数量。 Child 是一个 Parent 所以 Child 理想情况下也应该知道如何计算它自己的实例。如果 static 成员未被继承,您还必须在 Child 中复制 Parent 中的代码。

我认为编译器对待您的方式没有任何问题,无论您的方法是静态的还是其他方式。文档中接下来的几行说:

This rule allows for covariant return types - refining the return type of a method when overriding it. If R1 is not a subtype of R2, a compile-time unchecked warning occurs unless suppressed by the SuppressWarnings annotation (§9.6.3.5).

清除。 R1 必须是 R2 的子类型,就像 Integer 是另一个 IntegerNumberString 不是 void 的子类型。

文档说:"compile-time unchecked warning occurs..."。但是我注意到一个完整的编译错误正在等待。

继承仍然按预期工作。删除您的子方法,您将可以访问父方法,无论是静态方法还是其他方法。

这是 Java 中最奇怪的事情之一。假设我们有以下 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();    
}

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

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

JVM执行这段代码时,首先会在B中寻找方法foo()->Number;找不到方法时,直接superclassA查找,以此类推。 A.foo() 被发现并执行。

现在魔法开始了——B 的供应商发布了 B 的新版本,“覆盖”A.foo

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

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

这个神奇的功能为静态方法增加了一定程度的活力。但老实说,谁需要这个功能?可能没有人。它只会造成混乱,他们希望可以将其删除。

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

让我们再深入一点。假设 B 发布了另一个版本,"covariant return type"

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

据 B 所知,这可以很好地针对 A 进行编译。 Java 允许,因为 return 类型是 "covariant";这个功能比较新; 以前,"overriding" 静态方法必须具有相同的 return 类型。

这次 C.x 会是什么?是0.1f!因为JVM在B中没有找到foo()->Number;它位于 A。 JVM 将 ()->Number()->Integer 视为两种不同的方法,可能是为了支持 JVM 上 运行 的一些非 Java 语言。

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

现在,B 的供应商在听到所有投诉后,决定从 B 中删除 foo,因为它对 "override" 静态方法来说太危险了。我们从 B 和 运行 C 再次获取新的二进制文件(无需重新编译 C)- boom,运行时间错误,因为在 B 或 A 中找不到 B.foo()->Integer

这整个混乱表明允许静态方法具有 "covariant return type" 是设计疏忽,这实际上仅适用于实例方法。

更新 - 这个特性在某些用例中可能很有吸引力,例如,静态工厂方法 - A.of(..) returns A,而 B.of(..) returns 更具体 B。 API 设计者必须小心并推理潜在的危险用法。如果AB是同一个作者,又不能被用户转class,这样的设计还是比较安全的。