需要考虑的”初始化“或者说”调用“部分就4个:
- member
- method
- 特殊的method:constructor。
- 特殊的member:static member/block。
理解记忆方式:
- 对单独一个method来讲,它所在的类已经被构建了,所以它所在类的constructor一定是已经被调用了。(甚至可以展开说,method的被调用顺序是最靠后的,因为为了实现多态,它必须要late binding)
- 对constructor来讲,所有的member应该都是可用的,所以member的初始化要先于constructor。(成员变量可以看作是这个类的固有属性。要构建一个类,它的固有属性肯定是要事先准备好。就好比是,你要new一个desk类,那至少,这个desk的width, height这些固有属性要准备好吧,否则,我怎么知道应该建造一个多大的desk呢?)
- 对member来讲,和所在类息息相关的static部分,应该在定义类的阶段,最先就被初始化,所以它会优先于其它的member。
- 【另一方面,要保证所有member准备好,对于子类来讲,可以调用的父类的member也是应该被”正确“准备好的,而这一条只能由父类的constructor保证。所以父类的constructor要先于子类的member被调用。】对子类来讲,父类的所有东西都是准备好的,所以父类部分的初始化要先于子类。所以,父类的constructor要先于子类的member,以保证子类的member调用的父类public部分都被事先构建好了。(但父类的constructor会晚于子类的static,这是一个例外,见下一条)
- 由于static是和类一起同生共死,所以,即便是子类,在看到这个名字时对应的static就已经被初始化了,所以它”甚至“优先于父类的constructor,但会晚于父类的static(子类的static可能会调用父类的static,所以必须保证父类的static先被初始化)。
- 父类的初始化顺序,递归地受以上规则的控制。
坑
那是不是说掌握了这些原则就万事大吉了呢?我想并不是,更重要的是,你需要按照规范去编写代码。
但是,你并不总是可以遇到很规范的代码,往往会有奇怪的逻辑和用法。例如陈皓的一篇blog《JAVA构造时成员初始化的陷阱》就谈到了一个让人头痛的情况。我们做简要地考察:
public class Base
{
Base() {
preProcess();
}
void preProcess() {}
}
public class Derived extends Base
{
public String whenAmISet = "set when declared";
@Override void preProcess()
{
whenAmISet = "set in preProcess()";
}
}
public class Main
{
public static void main(String[] args)
{
Derived d = new Derived();
System.out.println( d.whenAmISet );
}
}
问的是,在main()
函数中,whenAmISet
的值应该是什么。
有了我们上面的分析做基础,我觉得你不应该直接去回答这个问题,而是应该反问,这段代码的逻辑是否有问题。
因为在Base
这个类中,构造函数竟然调用了一个method来做初始化,而且,这个method还在子类中被重写。
通过我们上面的逻辑,method的初始化应该放在最后,因为要支持late binding。就算要做初始化,也该把初始化的method设定为静态。可这里却反常地使用了一般的method。所以不得不说,这样编写代码的逻辑是否有问题。
再来就可以引述陈皓文中总结性的话了:
在语言设计的时候,“在构造函数中调用虚函数”是个两难的问题。
- 如果调用的是父类的函数的话,这个有点违反虚函数的定义。
- 如果调用的是子类的函数的话,这可能产生问题的:因为在构造子类对象的时候,首先调用父类的构造函数,而这时候如果去调用子类的函数,由于子类还没有构造完成,子类的成员尚未初始化,这么做显然是不安全的。
C++选择了第一种,而Java选择了第二种。
无论是哪一种,这种用法本身其实就是有问题的。我想,除了应付面试、考试,这样的编写方式就不应该让它存在。而不是反过来,去钻研它为什么是对的。这只不过是拿更多的错误去掩盖已成事实的错误。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。