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

2022-09-04 02:50:41

此代码无法编译,因为 in 的返回类型为 。StringstaticMethodChild

class Parent {
    static void staticMethod() {    
    }
}

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

我知道 JLS 8 在 §8.4.8.3 “重写和隐藏中的要求”中说:

如果返回类型为 R1 的方法声明 d1 覆盖或隐藏了返回类型为 R2 的另一个方法 d2 的声明,则 d1 必须是 d2 的返回类型可替换 (§8.4.5),否则会发生编译时错误。

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


答案 1

这是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();    
}

假设所有 3 个类都来自不同的供应商,具有不同的发布计划。

在 编译时,编译器知道该方法实际上是 from ,并且签名是 。但是,为调用生成的字节码不引用 ;相反,它引用了方法。请注意,返回类型是方法引用的一部分。CB.foo()Afoo()->NumberAB.foo()->Number

当 JVM 执行此代码时,它首先在 中查找方法;当找不到该方法时,将搜索直接超类,依此类推。 找到并执行。foo()->NumberBAA.foo()

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

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

我们从 B 获取了新的二进制文件,并再次运行我们的应用。(请注意,的二进制文件保持不变;它尚未针对新的 .) 进行重新编译。多田!- 现在在运行时!因为JVM的搜索在这段时间里结束了。CBC.x0.2ffoo()->NumberB

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

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

让我们进一步沿着这个兔子洞走下去。假设 B 发布了另一个版本,具有“协变返回类型”

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

据 B 所知,这编译得很好。Java允许它,因为返回类型是“协变”;这个功能比较新;以前,“重写”静态方法必须具有相同的返回类型。A

这次会是什么?是的!因为 JVM 找不到 ;它被发现在.JVM 考虑并作为 2 种不同的方法,可能支持一些在 JVM 上运行的非 Java 语言。C.x0.1ffoo()->NumberBA()->Number()->Integer

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

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

这整个混乱表明,允许静态方法具有“协变返回类型”是一个设计疏忽,这实际上只适用于示例方法。

UPDATE - 此功能在某些用例中可能很迷人,例如,静态工厂方法 - 返回,而返回更具体的.API 设计人员必须小心谨慎,并对潜在的危险用法进行推理。如果 和 来自同一作者,并且用户无法对它们进行子类化,则此设计非常安全。A.of(..)AB.of(..)BAB


答案 2

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

这种限制的症结在于,方法确实被继承了,但不能被覆盖。如果我们必须找到对返回类型的方法进行编译时检查的动机,那么真正的问题是为什么静态方法在Java中继承但不能被覆盖?staticJavastatic

答案很简单。

static方法不能是,因为它们属于 a 而不是 .如果你想知道这背后的动机,你可以看看这个问题,它已经有了一些答案。 允许继承方法,因为在无数情况下,子类希望重用方法而不必重复相同的代码。考虑一个计算类的实例数的粗略示例:overridenclassinstancestaticstatic

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知道如何计算自己创建的数量。 理想情况下,应该知道如何计算自己的实例。如果成员未被继承,则还必须在其中复制代码。instancesChildParentChildstaticParentChild


推荐