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

Kev*_*n K 12 java inheritance

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

在研究这个问题时,我偶然发现了这个问题,并对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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这是预期的输出:

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

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

Jon*_*eet 11

有时候很难不这样做.

Joda Time为例.它的Chronology类型层次结构非常深,但抽象AssembledChronology类基于你组装一堆"字段"(月份等)的想法.有一个非final方法,assembleFields在构造函数中调用,以便为该实例组合字段.

它们不能被传递给构造函数链,因为某些字段需要返回创建它们的时间顺序,稍后 - 并且您不能this在链式构造函数参数中使用它们.

我已经在Noda Time中使用了令人讨厌的长度以避免它实际上是一个虚拟的方法调用 - 但实际上它是非常相似的东西.

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

  • 就像施工点后的不变. (2认同)

Pau*_*ora 6

一个例子是非final(和package-private)方法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() {
}
Run Code Online (Sandbox Code Playgroud)

(HashMap来源)

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

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

  • +1代表查找.但这在基地并不意味着它是好的设计.我肯定会看到在执行子类的构造函数之前调用子类实例方法的风险.初始化的假设太容易了. (3认同)
  • 这是有趣的.看来这是必要的,因为构造函数中你提供了一个现有的`Map`,构造函数将添加现有map中的条目 - 在添加任何条目之前必须调用`init()`.至少他们使方法包私有,所以他们可以控制它.作为一个例子,我可以看到`LinkedHashMap`覆盖它来初始化链表头. (2认同)