【问题标题】:Initialization order of final fieldsfinal字段的初始化顺序
【发布时间】:2015-03-12 13:20:07
【问题描述】:

考虑这两个类:

public abstract class Bar {
    protected Bar() {
        System.out.println(getValue());
    }

    protected abstract int getValue();
}

public class Foo extends Bar {
    private final int i = 20;

    public Foo() {
    }

    @Override
    protected int getValue() {
        return i;
    }

    public static void main(String[] args) {
        new Foo();
    }
}

如果我执行 Foo,输出是 20。

如果我将字段设置为非最终字段,或者如果我在 Foo 构造函数中对其进行初始化,则输出为 0。

我的问题是:对于 final 字段,初始化顺序是什么?JLS 中在哪里描述了这种行为?

我希望找到一些关于 final 字段 here 的特殊规则,但除非我错过了什么,否则没有。

请注意,我知道我永远不应该从构造函数中调用可覆盖的方法。这不是问题的重点。

【问题讨论】:

    标签: java constructor final jls


    【解决方案1】:

    您的final int i 成员变量是一个常量变量:4.12.4. final Variables

    原始类型或String 类型的变量,即final 并使用编译时常量表达式(第15.28 节)进行初始化,称为常量变量

    这会对事物的初始化顺序产生影响,如12.4.2. Detailed Initialization Procedure 中所述。

    【讨论】:

    • 不得不删除我关于 final int编译时间常数 的评论(没有支持证据..正在寻找这个答案..谢谢)。 +1:P..
    • 好点。我错过了这个“常量变量”参考(多么矛盾的说法!)。但我仍然没有完全掌握对象初始化过程中的变化。这个常量变量是什么时候赋值的?详细的初始化过程讲的是类的初始化,讲的是“final 类变量”的初始化,而不是“final 实例变量”的初始化(除非我又错过了什么)。
    • 好的。我找到了。 docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.2 说:“某些表达式的值可以在编译时确定。这些是常量表达式(第 15.28 节)。”。 docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.28 说“引用常量变量(§4.12.4)的简单名称(§6.5.6.1)”是常量表达式。所以i 中的getValue() 是一个简单的名称,引用一个常量变量,因此它的值在编译时是已知的。
    • “详细初始化过程”的链接有点误导,因为它描述了类初始化,换句话说,仅与 static 字段相关。其实final实例字段和其他字段一样赋值,但相关的一点是getValue()从不读取字段,只是返回常量值。
    【解决方案2】:

    以“字节码-ish”的方式向您介绍它的外观。

    您应该已经知道,构造函数中的第一条实际指令必须是对super 的调用(无论是否带有参数)。

    当父构造函数完成并且超级“对象”完全构造时,该超级指令返回。因此,当您构造 Foo 时,会发生以下情况(按顺序):

    // constant fields are initialized by this point
    Object.construction // constructor call of Object, done by Bar
    Bar.construction // aka: Foo.super()
    callinterface getValue() // from Bar constructor
    // this call is delegated to Foo, since that's the actual type responsible
    // and i is returned to be printed
    Foo.construction
    

    如果你要在构造函数中初始化它,那将在“现在”发生,在 getValue() 已经被调用之后。

    【讨论】:

    • 我知道,但这并不能解释为什么当字段为 final 时 i 的值为 20 而不是时为 0。
    • JB 嗯......好吧,我真的不想偷 Jesper 的好答案
    • 当我阅读 JVM 规范时,它要求在父构造函数调用之前无法访问的代码可以对正在构建的对象执行任何操作除了将值存储到派生类字段。禁止可以绕过父构造函数的异常处理构造,尽管我不记得使用了哪些确切规则(例如,如果任何可以通过在父构造函数完成之前抛出的异常可访问的代码将被视为在没有父构造函数已被调用)。
    • 恕我直言,Java 语言(以及许多 .NET 语言)在构造链接方面有点乏力。在很多情况下,foo=new Bar(); 在不使用虚拟方法的情况下无法履行其职责,但没有一个好的模式可以让派生类在执行其唯一的机会之前检查其传入的参数,以确保其职责得到完全的。也许最干净的方法是使用一些构造函数,这些构造函数用于构造Bar 对象(并且不允许合法派生类链接到这些构造函数)...
    • ...并有其他构造函数,这些构造函数只应该用于构造派生对象(反过来,它们本身也应该遵循相同的模式)。不幸的是,我知道Bar 没有任何机制指定构造函数对于想要构造Bar 实例的代码应该是公共的,但不应该用于构造DerivedBar 实例的Bar 部分。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-21
    • 1970-01-01
    • 2017-05-31
    相关资源
    最近更新 更多