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

在Java中,是否有正当理由从类构造函数调用非final方法?,java,inheritance,Java,Inheritance,我最近花了相当多的时间调试生产代码中的一个问题,最终证明是由一个类在其构造函数中调用一个抽象方法引起的,该方法的子类实现试图使用一个尚未初始化的子类字段(下面包括一个说明这一点的示例) 在研究这一点时,我无意中发现了Jon Skeet的答案,并对其产生了兴趣: 一般来说,在构造函数中调用非final方法是不好的,因为正是这个原因-子类构造函数体还没有执行,所以您实际上是在一个尚未完全初始化的环境中调用方法 这让我想知道,有没有合法的理由从构造函数调用非最终或抽象方法?或者它几乎总是一个糟糕设计的

我最近花了相当多的时间调试生产代码中的一个问题,最终证明是由一个类在其构造函数中调用一个抽象方法引起的,该方法的子类实现试图使用一个尚未初始化的子类字段(下面包括一个说明这一点的示例)

在研究这一点时,我无意中发现了Jon Skeet的答案,并对其产生了兴趣:

一般来说,在构造函数中调用非final方法是不好的,因为正是这个原因-子类构造函数体还没有执行,所以您实际上是在一个尚未完全初始化的环境中调用方法

这让我想知道,有没有合法的理由从构造函数调用非最终或抽象方法?或者它几乎总是一个糟糕设计的标志

例子 以下是预期产出:

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


当然,问题是在第一次调用
method()
时,字段
arr
为空,因为它还没有初始化。

一个例子是非最终(和包私有)方法
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
源)

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


编辑:为了回应作者的评论,我并不是说它一定是好的设计,因为它在源代码中使用过。我只是想指出一个例子。我注意到每个
HashMap
构造函数都会注意最后调用
init()
,但这当然还是在子类构造函数之前。因此,子类实现承担了一定的责任,不能把事情搞砸。

这是个好问题。我投反对票,并将尝试将其纳入我未来的代码中。我认为调用一个抽象方法是非常糟糕的/有风险的。

有时很难不这样做

比如说。它的
年表
类型层次结构很深,但是抽象的
汇编年表
类基于这样一种想法,即您可以组合一组“字段”(月份等)。有一个非最终方法,
assembleFields
,在构造函数期间调用该方法,以便为该实例组装字段

它们不能在构造函数链中向上传递,因为一些字段稍后需要引用创建它们的时间顺序-并且不能在链式构造函数参数中使用
this

为了避免它实际上是一个虚拟的方法调用,我已经花了很长的时间,但老实说,它非常相似


如果可能的话,避免这种事情是个好主意。。。但有时这样做真的很麻烦,特别是如果你想让你的类型在构造之后是不可变的。

一般来说,在类被构造之前调用它的方法不是一个好的设计;但是,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();
    }
}

在很多方面,它看起来更干净。如果做不到这一点,则始终能够通过工厂以适当的“初始化顺序”构造对象。

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

我假设,为了讨论这个问题,您不必担心静态方法。所以问题是“从构造函数调用非最终、非静态方法是否合适。”看看这个答案:@Zaki这是一个派生类在基中调用代码的极好例子,但不是相反。基类从其构造函数调用抽象(或非最终)方法的地方。@JohnB是的,我指的是非静态方法。@Zaki看起来他们最终通过将方法调用移出构造函数解决了问题。有意思,但我的问题是,在某些情况下,从构造函数调用方法是必需的还是难以避免的。但这并不意味着它是一个好的设计。我肯定看到在执行子类的构造函数之前调用子类实例方法的风险。有一些初始化的假设太容易了,这很有趣。这似乎是必要的,因为构造函数提供了一个现有的
映射
,并且构造函数将添加现有映射中的条目-
init()
必须在添加任何条目之前调用。至少他们将方法包设置为私有,以便对其进行一些控制。例如,我可以看到
LinkedHashMap
覆盖它以初始化链表头。将子类构造函数代码保留在构造函数中,而只保留在子类的构造函数中如何?施工后仍然不变;子类构造函数必须设置的唯一警告
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();
    }
}