我最近花了几分钟调试生产代码中的问题,最终结果是由一个类在其构造函数中调用抽象方法引起的,并且该方法的子类实现尝试使用子类字段还没有被初始化(一个例子说明了这一点,包括在下面)
在研究这个时,我偶然发现了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
为空,因为它尚未初始化。
答案 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
方法。这允许子类影响基类的配置。