在Java中,是否有合理的理由从类构造函数中调用非final方法?

我最近花了几分钟调试生产代码中的一个问题,最终结果是由一个在其构造函数中调用抽象方法的类引起的,并且该方法的子类实现试图使用一个尚未生成的子类字段已初始化(下面列出了一个说明这一点的例子)

在研究这个问题时,我偶然发现了这个问题 ,并对Jon Skeet的回答很感兴趣:

一般来说,在构造函数中调用非final方法是一个坏主意,正是因为这个原因 – 子类构造函数体还没有被执行,所以你有效地在一个尚未完全调用的环境中调用一个方法初始化。

这让我感到疑惑,是否有合理的理由从构造函数中调用非final或抽象方法? 或者它几乎总是设计糟糕的迹象?

public class SSCCE { static abstract class A { public A() { method(); // Not good; field arr in B will be null at this point! } abstract void method(); } static class B extends A { final String[] arr = new String[] { "foo", "bar" }; public B() { super(); System.out.println("In B(): " + Arrays.toString(arr)); } void method() { System.out.println("In method(): " + Arrays.toString(arr)); } } public static void main(String[] args) { new B().method(); } } 

这是预期的输出:

在method()中:null
在B():[foo,bar]
在方法()中:[foo,bar]

当然,问题是在第一次调用method() ,字段arr为null,因为它尚未初始化。

有时候很难不这样做。

以Joda Time为例。 它的年代表类型层次结构很深,但抽象的AssembledChronology类基于你组装一堆“字段”(月份等)的想法。 在构造函数中调用了一个非final方法assembleFields ,以便为该实例组合字段。

它们不能传递给构造函数链,因为某些字段需要引用回创建它们的时间顺序,以后 – 并且你不能在链式构造函数参数中使用this

我已经在Noda Time中讨厌了,以避免它实际上是一个虚拟的方法调用 – 但是说实话,它是非常相似的。

如果你可能的话,避免这种事情是个好主意……但有时这样做真的很痛苦,特别是如果你希望你的类型构造是不可变的。

一个例子是非final(和包私有)方法HashMap#init() ,这是一个空方法,它的目的是为了被子类覆盖的确切目的:

 /** * Initialization hook for subclasses. This method is called * in all constructors and pseudo-constructors (clone, readObject) * after HashMap has been initialized but before any entries have * been inserted. (In the absence of this method, readObject would * require explicit knowledge of subclasses.) */ void init() { } 

(来自HashMap源码)

我没有关于子类如何使用它的任何示例 – 如果有人这样做,请随时编辑我的答案。

编辑:回应@John B的评论,我不是说它必须是好的设计,因为它在源中使用。 我只想指出一个例子。 我注意到每个HashMap构造函数最后都会调用init() ,但这当然仍然在子类构造函数之前。 因此,对子类实现的责任在于不要搞砸了。

通常,在构造类之前调用​​类的方法并不好; 但是,Java允许在您知道自己在做什么的情况下例外(即,您不访问未初始化的字段)。 使用抽象方法,我认为不可能“知道”你在父类中做了什么。

上面的代码可以通过对“一个类处理它的职责”的更严格的解释来轻松解决。 初始化子类不是超类的责任,因此在初始化可能完成之前调用子类代码不应该是超类的特权。

是的,它是在JDK(如HashMap代码)中完成的,它使用特殊的“init()”方法来暗示所有子类代码的初始化; 但是,我会提出以下呼叫模式更清晰,并允许更多的灵活性。

 public class SSCCE { static abstract class A { public A() { } abstract void method(); } static class B extends A { final String[] arr = new String[] { "foo", "bar" }; public B() { super(); method(); System.out.println("In B(): " + Arrays.toString(arr)); } void method() { System.out.println("In method(): " + Arrays.toString(arr)); } } public static void main(String[] args) { new B().method(); } } 

它在很多方面看起来都那么清洁。 如果做不到这一点,总有能力通过工厂以适当的“初始化顺序”构建对象。

好问题。 我投了“不”,并试图将其纳入我未来的代码中。 我认为调用抽象方法是非常糟糕的forms/风险。

如果子类需要在构造期间执行应该执行的工作,它可以覆盖父构造函数并在其自己的构造函数中执行。

一个非常有用的模式是调用抽象(或重写)的createX方法。 这允许子类影响基类的配置。