首先,通常在构造函数中调用方法没有问题。这些问题特别涉及调用构造函数的类的可重写方法以及将对象的
this引用传递给其他对象的方法(包括构造函数)的特殊情况。
避免重载方法和“泄漏
this” 的原因可能很复杂,但它们基本上都与防止使用未完全初始化的对象有关。
避免调用可覆盖的方法
避免在构造函数中调用可重写方法的原因是Java语言规范(JLS)第12.5节中定义的实例创建过程的结果。
除其他事项外,第12.5节的过程确保了在实例化派生类[1]时,对其基类的初始化(即,将其成员设置为其初始值并执行其构造函数)在其自身的初始化之前进行。这旨在通过两个关键原则允许一致的类初始化:
- 每个类的初始化都可以专注于仅初始化它明确声明的成员,这是安全的,因为要知道从基类继承的所有其他成员都已被初始化。
- 每个类的初始化都可以安全地使用其基类的成员作为其自身成员的初始化的输入,因为可以确保在初始化该类时已对其进行了正确的初始化。
但是,有一个陷阱:Java允许在构造函数中进行动态分派[2]。这意味着,如果作为派生类实例化的一部分执行的基类构造函数调用了派生类中存在的方法,则会在该派生类的上下文中调用该方法。
所有这些的直接结果是,在实例化派生类时,将在派生类初始化之前调用基类构造函数。如果该构造函数调用了被派生类覆盖的方法,则 即使派生类尚未初始化 ,
也将 调用派生类方法(而不是基类方法)。显然,如果该方法使用派生类的任何成员,则这是一个问题,因为它们尚未初始化。
显然,问题是基类构造函数调用方法的结果,而派生类可以重写这些方法。为防止此问题,构造函数应仅调用自己的最终,静态或私有类的方法,因为这些方法不能被派生类覆盖。最终类的构造函数可以调用其任何方法,因为(根据定义)它们不能从其派生。
JLS的示例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。本示例中的事件顺序如下:
new Test()
在main()
方法中被调用。- 由于
Test
没有显式构造函数,因此Super()
将调用其超类的默认构造函数(即)。 - 该
Super()
构造函数调用printThree()
。这将分派到Test
类中方法的重写版本。 - 该类的
printThree()
方法将Test
打印three
成员变量的当前值,这是默认值0
(因为Test
尚未初始化实例)。 - 的
printThree()
方法和Super()
构造每个出口,和Test
实例被初始化(在该点处three
的一个设置到3
)。 - 该
main()
方法printThree()
再次调用,这一次将打印期望值3
(因为Test
实例已被初始化)。
如上所述,第12.5节指出,(2)必须在(5)之前发生,以确保
Super早在…之前初始化
Test。但是,动态分派意味着(3)中的方法调用在未初始化的
Test类的上下文中运行,从而导致意外的行为。
避免泄漏 this
禁止
this从构造函数传递到另一个对象的限制更容易解释。
基本上,在构造函数完成执行之前,不能认为对象已完全初始化(因为其目的是完成对象的初始化)。因此,如果构造函数将对象的传递
this给另一个对象,则该另一个对象将具有对该对象的引用,即使该对象尚未完全初始化(因为其构造函数仍在运行)。如果另一个对象然后尝试访问未初始化的成员或调用依赖于其完全初始化的原始对象的方法,则可能会导致意外行为。
有关如何导致意外行为的示例,请参阅本文。
[1]从技术上讲,Java中的每个类除外
Object都是派生类-我仅在这里使用术语“派生类”和“基类”来概述所讨论的特定类之间的关系。
[2]在JLS中(据我所知)没有任何理由说明这种情况。另一种选择-禁止在构造函数中进行动态分派-将使整个问题变得毫无意义,这可能正是C
++不允许这样做的原因。



