为什么在子类上调用在其超类中声明的静态方法时,不会调用子类的静态初始化程序?
鉴于以下类别:
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的子类,S在S上调用静态方法。
我认为这与jvm规范的这一部分有关:
每个帧(第2.6节)包含对运行时常量池(第2.5.5节)的引用,该引用用于支持方法代码的动态链接的当前方法的类型。 方法的类文件代码是指要调用的方法和要通过符号引用访问的变量。 动态链接将这些符号方法引用转换为具体的方法引用,根据需要加载类以解析尚未定义的符号,并将变量访问转换为与这些变量的运行时位置相关联的存储结构中的适当偏移。
方法和变量的这种后期绑定使得方法使用的其他类中的更改不太可能破坏此代码。
在jvm规范的第5章中,他们还提到:可以初始化类或接口C,其中包括:
执行任何一个引用C(§new,§getstatic,§putstatic,§invokestatic)的Java虚拟机指令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
在标题中要小心,静态字段和方法不会被inheritance。 这意味着当您在Sub
注释staticMethod()
时, Sub.staticMethod()
实际上调用Super.staticMethod()
然后不执行Sub
静态初始化程序。
然而,这个问题比我第一眼看到的更有趣:在我看来,这不应该在没有警告的情况下编译,就像在类的实例上调用静态方法时一样。
编辑:正如@GeroldBroser指出的那样,这个答案的第一个陈述是错误的。 静态方法也是inheritance的,但从不覆盖,只是隐藏。 我要留下历史的答案。
JLS特别允许JVM避免加载Sub类,它在问题中引用的部分中:
对静态字段的引用(第8.3.1.1节)仅导致实际声明它的类或接口的初始化,即使它可能通过子类的名称,子接口或实现接口的类来引用。
原因是避免不必要地使用JVM加载类。 初始化静态变量不是问题,因为它们无论如何都不会被引用。
原因很简单:JVM不要过早地做额外的工作(Java本质上是懒惰的)。
无论是编写Super.staticMethod()
还是Sub.staticMethod()
,都会调用相同的实现。 而这个父实现的实现通常不依赖于子类。 Super
静态方法不应该访问Sub
成员,那么初始化Sub
的重点是什么呢?
你的例子似乎是人为的,没有精心设计。
使子类重写超类的静态字段听起来不是一个好主意。 在这种情况下,Super的方法的结果将取决于首先触及哪个类。 这也很难让Super的多个孩子有自己的行为。 简而言之,静态成员不是为了多态 – 这就是OOP原则所说的。
由于某种原因,jvm认为静态块不好,而且它没有被执行
我相信,这是因为你没有为子类使用任何方法,所以jvm认为没有理由“初始化”类本身,方法调用在编译时静态绑定到父类 – 静态方法有后期绑定
static { System.out.println("init"); staticVar = new Object(); }
添加一些其他方法,并在sub之前调用它
Sub.someOtherMethod(); new UsersClass().method();
或者做显式的Class.forName("Sub");
Class.forName("Sub"); new UsersClass().method();
静态块执行时静态初始化程序
初始化类时,将执行类中声明的静态初始化程序
当你调用Sub.staticMethod();
这意味着课程没有初始化。你只是参考
初始化类时
在Java中初始化类之后在类加载之后,将发生类的初始化,这意味着初始化类的所有静态成员。 在以下情况下,Java在Java中初始化:
1)使用new()关键字或使用class.forName()的reflection创建类的实例,这可能会在Java中抛出ClassNotFoundException。
2)调用Class的静态方法。
3)分配类的静态字段。
4)使用类的静态字段,它不是常量变量。
5)如果Class是顶级类,则执行词法中嵌套在类中的断言语句。
在JVM – Java中加载和初始化类时
这就是你得到null(实例变量的默认值)的原因。
public class Sub extends Super { static { staticVar = new Object(); } public static void staticMethod() { Super.staticMethod(); } }
在这种情况下,类是初始化,你得到new object()
哈希码。如果你不覆盖staticMethod()
意味着你的引用超类方法和Sub
类没有初始化。
根据这篇文章 ,当你调用静态方法或使用类的静态字段时,只会初始化该类。
以下是屏幕截图示例。