【问题标题】:Can a java subclass's private final field be initialized before the super constructor completes?java子类的private final字段可以在超级构造函数完成之前初始化吗?
【发布时间】:2012-06-30 22:48:33
【问题描述】:

我有一对看起来像这样的类;

public abstract class Class1 {

//...

    public Class1() {
        //...
        function2();
        //...
    }

    protected abstract void function2();

}

public class Class2 implements Class1 {

    private final OnSomethingListener mOnSomethingListener = new OnSomethingListener() {
        @Override
        onSomething() {
            doThatOtherThing();
        }
    }

    protected void function2() {
        //uses mOnSomethingListener
        //however mOnSomethingListener is null when this function is called from super()
        //...
    }

    public Class2() {
        super();
    }
}

我假设侦听器为空,因为我有效地从super() 引用它并且它尚未实例化。但是,我想将其设为final,因为它确实如此。我可以让这个字段(监听器)及时初始化而不把它放在超类中(永远不会使用监听器)吗?

【问题讨论】:

  • 拥有正确的行为应该胜过“尽可能使用final”。我只是为延迟初始化该字段的侦听器使用吸气剂。这使得初始化的顺序非常清晰和明确,而不依赖于技术性。或者,您可以在构造函数之后运行第二个初始化函数并调用 function2(),但我仍然认为这不是比删除 final 更好的选择。
  • 超类将在其子类之一之前被初始化。
  • @Jean-ChristopheFortin:如果超类需要子类直接管理的对象,这是一个很好的解决方案。在这种情况下,OP 似乎希望超类不知道 function2() 究竟做了什么或需要运行什么。

标签: java null super construction


【解决方案1】:

简短的回答 - 不。超类的构造函数、字段初始化器和实例初始化器总是在子类之前调用​​。

调用顺序在section 8.8.7.1 of the JLS 中正式定义。总结相关的最后部分(其中S 是超类,C 是子类):

在确定 i 相对于 S 的直接封闭实例(如果有)之后,超类构造函数调用语句的求值通过从左到右求构造函数的参数进行,就像在普通方法调用中一样;然后调用构造函数。

最后,如果超类构造函数调用语句正常完成,则执行C的所有实例变量初始化器和C的所有实例初始化器。如果一个实例初始化器或实例变量初始化器 I 在文本上在另一个实例初始化器或实例变量初始化器 J 之前,则 I 在 J 之前执行。

因此,当超类构造函数运行时,子类及其所有字段都完全未初始化。出于这个原因,从构造函数调用重写的方法是不好的做法。您实际上是在让对对象的引用从其构造函数中“转义”,这意味着所有构造保证都已关闭(包括诸如最终字段更改值之类的事情)。

从构造函数调用 abstract 方法几乎总是错误的做法。根据子类中方法的实现,在某些情况下您可能会侥幸逃脱(即,如果该方法根本不依赖于任何状态),但它几乎肯定会导致难以调试的失败在某个时候。

例如,您是否期望以下之间存在差异:

protected String function2() {
    return "foo";
}

private final String foo = "foo";

protected String function2() {
    return foo;
}

分析这样的问题很困难,因为它们打破了课堂运作方式的思维模式。最好完全避免这种情况。

【讨论】:

    【解决方案2】:

    超类总是在子类之前初始化。只能先初始化子类的静态字段。

    你可以从超类调用一个被覆盖的方法,然后访问一个未初始化的字段。这被认为是不好的做法。

    【讨论】:

      【解决方案3】:

      您的设计是“泄露this 问题”的一个实例,并且是Java 中的反模式。永远不要从构造函数中调用可公开覆盖的方法。

      【讨论】:

      • 一个典型的解决方法是一个两步过程,你首先实例化然后初始化。
      • 如果你正在使用像 Wicket 这样的东西,它依赖于从构造函数进行所有对象初始化,这往往会成为一个问题。 (我可以理解,不得不使用单独的初始化器感觉很不自然。)
      • 我在 Android 上,我需要将 Views 添加到 ViewGroup 层次结构中,有时我需要添加一些额外的层次结构。我已经通过在子构造函数中使用回调解决了实际问题,但我明白为什么这可能是个坏主意,是的!
      • @millimoose 正确的做法是工厂方法,从外部可以看成是单点初始化。如果 Wicket 不支持这一点,那就是软弱的表现。
      • @MarkoTopolnik 或者当权衡不值得时,不要在 API 表面抛出每一个模式的迹象。 “泄露的this 问题”并不经常发生,并且可以通过惰性初始化程序轻松缓解,这是一个有效的设计选择,不想让您的 API 碎片化和不直观。
      猜你喜欢
      • 1970-01-01
      • 2019-01-21
      • 2010-10-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-30
      相关资源
      最近更新 更多