【问题标题】:why static block in child class does not get executed?为什么子类中的静态块没有被执行?
【发布时间】:2014-08-01 23:14:46
【问题描述】:

这里是代码

public class ClassResolution {
static class Parent {
    public static String name;
    static {
        System.out.println("this is Parent");
        name = "Parent";
    }

}

static class Child extends Parent {
    static {
        System.out.println("this is Child");
        name = "Child";
    }

}

public static void main(String[] args) throws ClassNotFoundException {
    System.out.println(Child.name);
}}

我期望的输出是:

this is Parent
this is Child
Child

但实际上是:

this is Parent
Parent

似乎 Child 类中的静态块没有被执行,但为什么呢?这是反直觉的,不是吗?

补充:
为了更清楚,我在下面列出了 2 1 点:

  1. 正如@axtavt 所说,根据JLS 12.4.1,Child 类已加载,但未初始化。
  2. 但是@Alexei Kaigorodov 指出,根据jvms-5.5, 类 Child 应该被初始化,因为执行 在 Child 类上指令 getstatic。

你怎么看?

补充2:
@Alexei Kaigorodov 已经更新了他的想法,所以似乎没有分歧。但我认为 Alexei Kaigorodov 的观点很有启发性,所以我把它留在那里。

谢谢大家。

【问题讨论】:

  • 我编辑了答案,不需要初始化 Child 类(尽管可能会发生)。

标签: java static-block


【解决方案1】:

由于Child.name 真的是Parent.name,所以不需要Child。

你可能会觉得这很有趣。

public class ClassResolution {
    static class Parent {
        public static String name;

        static {
            System.out.println("this is Parent");
            name = "Parent";
        }

    }

    static class Child extends Parent {
        static {
            System.out.println("this is Child");
            name = "Child";
        }

        static String word ="hello";
    }

    public static void main(String[] args) {
        System.out.println(Child.name);
        System.out.println(Child.word);
        System.out.println(Child.name);
    }
}

打印

this is Parent
Parent
this is Child
hello
Child

此类的javap 打印出对Child 的引用被保留。

C:\>javap -c -classpath . ClassResolution
Compiled from "ClassResolution.java"
public class ClassResolution {
  public ClassResolution();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: getstatic     #3                  // Field ClassResolution$Child.name:Ljava/lang/String;
       6: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       9: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: getstatic     #5                  // Field ClassResolution$Child.word:Ljava/lang/String;
      15: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      18: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      21: getstatic     #3                  // Field ClassResolution$Child.name:Ljava/lang/String;
      24: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: return
}

【讨论】:

  • 但是子类被加载了,我认为静态块应该在加载类时执行,还是在执行时执行?
  • 子类已加载,如果我删除 ClassResolution$Child.class 文件并运行 ClassResolution,我将得到 ClassNotFoundException。
  • Child 在字节码中被引用,但大多数事情都是尽可能懒惰地完成的。在这种情况下,JVM 不会初始化 Child 来访问其父类中的字段。
  • 它没有加载。编译器从字节码中消除了对 Child 的所有引用。
  • 干得好,彼得!答案似乎很明显,但故事发生了一个启发性的转折。
【解决方案2】:

简而言之,Child.name 等于Parent.name,编译器就是这样编译的。

因为name是Parent类的静态字段,所以是Parent中的类方法,不是Child。 Java 编译器提供了一种快捷方式,子类可以调用静态父类方法/字段,就好像它们来自自己的类一样,但在内部它们是根据父类编译的。

虽然您的代码引用了Child.name,但它在内部是Parent.name,由编译器处理。然后,由于 Child 类没有被初始化,&lt;clinit&gt; 静态初始化块永远不会运行,name 仍然是“Parent”。

【讨论】:

    【解决方案3】:

    来自JLS 12.4.1:

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

    • T 是一个类,并创建了一个 T 的实例。
    • T 是一个类,由 T 声明的静态方法被调用。
    • 分配了由 T 声明的静态字段。
    • 使用了由 T 声明的静态字段,并且该字段不是常量变量(第 4.12.4 节)。
    • T 是一个顶级类,并且执行一个在词法上嵌套在 T 中的断言语句(第 14.10 节)。

    如您所见,这些都不会在您的代码中发生(注意 name 是在 Parent 中声明的,而不是在 Child 中声明的),因此 Child 不会被初始化,它的静态块也不会不会被处决。

    如果你做一些事情来触发Child的初始化,你会得到一个预期的输出:

    new Child();
    System.out.println(Child.name); 
    

    但是请注意,静态字段不会被继承,因此Child.name 和Parent.name 实际上指的是同一个字段。这就是为什么在实践中使用与您的示例类似的代码没有多大意义。

    另请注意,尽管Child.name 实际上是指Parent.name,但它在字节码中仍被引用为Child.name,因此您的代码会触发Child 的加载,但不会触发其初始化。

    【讨论】:

    • 所以,这意味着子类已加载,但未初始化,对吧?
    • 这很合理,但很混乱!!
    【解决方案4】:

    JLS#12.4.1. When Initialization Occurs

    对静态字段的引用(第 8.3.1.1 节)只会初始化实际声明它的类或接口,即使它可能通过子类、子接口或实现的类的名称来引用一个界面。

    我想上面已经说明了一切..

    【讨论】:

    • 这是因为编译器保留直接引用该字段的权利。但是在我们的例子中,对 Child 的引用被保留了,所以我们必须阅读 JVM 规范:docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.5“在执行 getstatic、putstatic 或 invokestatic 指令时,声明已解析字段或方法的类或接口被初始化,如果它尚未初始化。”在代码中我们看到getstatic so/ClassResolution$Child.name 指令。
    【解决方案5】:

    糟糕,我错了,对 Child 的引用没有被删除,并且类确实被加载了,但没有初始化。恭喜,您发现了一个 JVM 错误。转到 Oracle 站点并归档。如果您不想这样做,请告诉我,我会自己做。

    编辑:我又错了,请参阅下面的评论。这不是一个错误,这是一个“陷阱”。

    【讨论】:

    • 正如 axtavt 指出的那样,JLS 指定了何时初始化一个类,我认为它在加载时也已初始化。所以看起来这不是一个错误,而是一个陷阱!
    • 您到底发现了什么是错误?一个类在没有初始化的情况下加载的事实?那肯定不是错误,可以随时加载一个类。即使没有引用类中的字段也会产生错误的事实?在加载类之前,运行时不会知道它是否拥有引用的字段,所以再次没有错误。
    • @Marko Topolnik 查看我对 Amid 回答的评论
    • 也许你是对的,它与 JVM 规范冲突!其实是你发现了这个bug,我刚遇到!
    • 在仔细阅读了所有相关的 JVM 规范部分之后,我不得不说这是一个误报。断言“在执行 getstatic ... 声明已解析字段 ... 的类 ... 已初始化”需要初始化声明已解析字段 (Parent) 的类,并且已解析字段未引用字段在我们的例子中。字段解析本身需要创建引用类(Child),但不需要初始化。
    猜你喜欢
    • 2016-05-11
    • 1970-01-01
    • 2018-08-25
    • 1970-01-01
    • 1970-01-01
    • 2016-05-15
    • 1970-01-01
    • 2012-12-25
    相关资源
    最近更新 更多