我最近花了几分钟调试生产代码中的问题,最终结果是由在其构造函数中调用抽象方法的类引起的,并且该方法的子类实现尝试使用尚未生成的子类字段已初始化(下面列出了一个说明这一点的例子)
在研究这个问题时,我偶然发现了这个问题,并对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中使用了令人讨厌的长度以避免它实际上是一个虚拟的方法调用 - 但实际上它是非常相似的东西.
如果你可能的话,避免这种事情是个好主意......但有时这样做真的很痛苦,特别是如果你希望你的类型在构造后是不可变的.
一个例子是非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,但这当然仍然在子类构造函数之前.因此,对子类实现的责任很大,不要搞砸了.
归档时间: |
|
查看次数: |
2272 次 |
最近记录: |