为什么在子类上调用在其超类中声明的静态方法时,不调用该子类的静态初始值设定项?

2022-09-02 21:50:53

给定以下类:

public abstract class Super {
    protected static Object staticVar;

    protected static void staticMethod() {
        System.out.println( staticVar );
    }
}

public class Sub extends Super {
    static {
        staticVar = new Object();
    }

    // Declaring a method with the same signature here, 
    // thus hiding Super.staticMethod(), avoids staticVar being null
    /*
    public static void staticMethod() {
        Super.staticMethod();
    }
    */
}

public class UserClass {
    public static void main( String[] args ) {
        new UserClass().method();
    }

    void method() {
        Sub.staticMethod(); // prints "null"
    }
}

我不是针对诸如“因为它在JLS中是这样指定的”之类的答案。我知道这是,因为JLS,12.4.1当初始化发生时,只读:

类或接口类型 T 将在首次出现以下任一项之前立即初始化:

  • ...

  • T 是一个类,调用由 T 声明的静态方法。

  • ...

我感兴趣的是,为什么没有这样的句子,是否有充分的理由:

  • T 是 S 的子类,在 T 上调用 S 声明的静态方法。

答案 1

在标题中要小心,静态字段和方法不会被继承。这意味着当您在 中注释时,实际上调用时,不会执行静态初始值设定项。staticMethod()SubSub.staticMethod()Super.staticMethod()Sub

但是,这个问题比我第一眼想象的更有趣:在我看来,这不应该在没有警告的情况下编译,就像在类的实例上调用静态方法一样。

编辑:正如@GeroldBroser所指出的,这个答案的第一个陈述是错误的。静态方法也是继承的,但从不重写,只是隐藏。我把答案留给历史。


答案 2

我认为这与jvm规范的这一部分有关:

每个帧 (§2.6) 都包含对运行时常量池 (§2.5.5) 的引用,用于当前方法的类型,以支持方法代码的动态链接。方法的类文件代码是指要调用的方法和要通过符号引用访问的变量。动态链接将这些符号方法引用转换为具体的方法引用,根据需要加载类以解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置关联的存储结构中的适当偏移量。

方法和变量的这种后期绑定使得方法使用的其他类中的更改不太可能破坏此代码。

在jvm规范的第5章中,他们还提到:类或接口C可以被初始化,除其他外,由于:

执行任何一个引用 C 的 Java 虚拟机指令 new、getstatic、putstatic 或 invokestatic(§new、§getstatic、§putstatic、§invokestatic)。这些指令通过字段引用或方法引用直接或间接地引用类或接口。

...

在执行 getstatic、putstatic 或 invokestatic 指令时,声明已解析字段或方法的类或接口(如果尚未初始化),则会对其进行初始化。

在我看来,文档的第一部分指出,任何符号引用都只是简单地解决和调用,而不考虑它来自哪里。关于方法解析的本文档对此有如下说明:

[M]ethod解析尝试在C及其超类中定位引用的方法:

如果 C 声明了一个具有方法引用指定的名称的方法,并且该声明是签名多态方法 (§2.9),则方法查找成功。描述符中提到的所有类名都将被解析 (§5.4.3.1)。

解析的方法是签名多态方法声明。C 不必使用方法引用指定的描述符声明方法。

否则,如果 C 声明的方法具有方法引用指定的名称和描述符,则方法查找将成功。

否则,如果 C 具有超类,则在 C 的直接超类上递归调用方法解析的步骤 2。

因此,它从子类调用的事实似乎被忽略了。为什么这样做?在您提供的文档中,他们说:

其目的是类或接口类型具有一组初始值设定项,这些初始值设定项将其置于一致状态,并且此状态是其他类观察到的第一个状态。

在您的示例中,当 Sub 静态初始化时,您将更改 Super 的状态。如果在调用 Sub.staticMethod 时进行了初始化,则 jvm 认为相同的方法会得到不同的行为。这可能是他们谈论避免的不一致。

此外,下面是一些执行 staticMethod 的反编译类文件代码,显示了 invokestatic 的使用:

Constant pool:
    ...
    #2 = Methodref          #18.#19        // Sub.staticMethod:()V

... 

Code:
  stack=0, locals=1, args_size=1
     0: invokestatic  #2                  // Method Sub.staticMethod:()V
     3: return

推荐