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

时间:2011-09-19 21:32:15

标签: java inheritance

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

在研究这个时,我偶然发现了this question,并对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为空,因为它尚未初始化。

6 个答案:

答案 0 :(得分:11)

有时候很难不这样做。

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

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

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

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

答案 1 :(得分:6)

一个例子是非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(),但这当然仍然在子类构造函数之前。因此,对子类实现的责任很大,不要搞砸了。

答案 2 :(得分:3)

通常,在构造类之前调用​​类的方法并不好;但是,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();
    }
}
它在很多方面看起来都那么干净了。如果做不到这一点,总是能够通过工厂以适当的“初始化顺序”构建对象。

答案 3 :(得分:0)

好问题。我投了“不”,并试图将其纳入我未来的代码中。我认为调用抽象方法会形成/风险很大。

答案 4 :(得分:0)

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

答案 5 :(得分:0)

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