首先,一般来说,在构造函数中调用方法没有问题.这些问题特别针对调用构造函数类的可覆盖方法的特定情况,以及将对象的this引用传递给其他对象的方法(包括构造函数).
避免可覆盖方法和“泄漏”的原因可能很复杂,但它们基本上都与防止使用未完全初始化的对象有关.
避免调用可覆盖的方法
避免在构造函数中调用可覆盖方法的原因是Java语言规范(JLS)§12.5中定义的实例创建过程的结果.
除此之外,§12.5的过程确保在实例化派生类[1]时,其基类的初始化(即将其成员设置为其初始值并执行其构造函数)在其自己的初始化之前发生.这旨在通过两个关键原则允许类的一致初始化:
>每个类的初始化可以专注于初始化它显式声明自己的成员,安全地知道从基类继承的所有其他成员都已经初始化.
>每个类的初始化可以安全地使用其基类的成员作为其自身成员初始化的输入,因为它保证在类的初始化发生时已经正确初始化.
但是,有一个问题:Java允许构造函数中的动态调度[2].这意味着如果作为派生类实例化的一部分执行的基类构造函数调用派生类中存在的方法,则在该派生类的上下文中调用它.
所有这一切的直接后果是,在实例化派生类时,在初始化派生类之前调用基类构造函数.如果该构造函数调用被派生类重写的方法,则它是被调用的派生类方法(不是基类方法),即使派生类尚未初始化.显然,如果该方法使用派生类的任何成员,则这是一个问题,因为它们尚未初始化.
显然,问题是基类构造函数调用方法的结果,这些方法可以被派生类覆盖.为了防止这个问题,构造函数应该只调用自己的类的方法,这些方法是final,static或private,因为这些方法不能被派生类覆盖.最终类的构造函数可以调用它们的任何方法,因为(根据定义)它们不能从中派生出来.
JLS的Example 12.5-2是这个问题的一个很好的证明:
class Super {
Super() { printThree(); }
void printThree() { System.out.println("three"); }
}
class Test extends Super {
int three = (int)Math.PI; // That is, 3
void printThree() { System.out.println(three); }
public static void main(String[] args) {
Test t = new Test();
t.printThree();
}
}
此程序打印0然后3.此示例中的事件序列如下:
>在main()方法中调用new Test().
>由于Test没有显式构造函数,因此调用其超类(即Super())的默认构造函数.
> Super()构造函数调用printThree().这将被分派到Test类中方法的重写版本.
> Test类的printThree()方法打印三个成员变量的当前值,这是默认值0(因为Test实例尚未初始化).
> printThree()方法和Super()构造函数各自退出,并初始化Test实例(此时将三个设置为3).
> main()方法再次调用printThree(),这次打印期望值为3(因为Test实例现已初始化).
如上所述,§12.5规定(2)必须在(5)之前发生,以确保在Test之前初始化Super.但是,动态分派意味着(3)中的方法调用在未初始化的Test类的上下文中运行,从而导致意外行为.
避免泄漏这个
将此从构造函数传递到另一个对象的限制更容易解释.
基本上,在构造函数完成执行之前,不能将对象视为完全初始化(因为其目的是完成对象的初始化).因此,如果构造函数将对象的this传递给另一个对象,那么另一个对象就会引用该对象,即使它尚未完全初始化(因为它的构造函数仍在运行).如果另一个对象然后尝试访问未初始化的成员或调用依赖于其完全初始化的原始对象的方法,则可能导致意外行为.
有关如何导致意外行为的示例,请参阅this article.
[1]从技术上讲,除了Object之外,Java中的每个类都是派生类 – 我只是在这里使用术语“派生类”和“基类”来概述所讨论的特定类之间的关系.[2]在JLS中没有理由(据我所知)为什么会出现这种情况.替代方案 – 不允许构造函数中的动态调度 – 会使整个问题没有实际意义,这可能就是C不允许的原因.