在Java中,为什么从构造函数内部调用方法被认为是不良实践?如果该方法计算量很大,那么情况是否尤其严重?
然而,有一个问题:Java允许构造函数中进行动态分派[2]。这意味着,如果作为派生类实例化的一部分执行的基类构造函数调用了派生类中存在的方法,则该方法在派生类的上下文中调用。
所有这些的直接结果是,在实例化派生类时,先调用基类构造函数,然后再初始化派生类。如果该构造函数调用了派生类覆盖的方法,则调用的是派生类方法(而不是基类方法),即使派生类尚未初始化。显然,如果该方法使用派生类的任何成员,由于它们尚未初始化,这就是一个问题。
显然,这个问题是由基类构造函数调用可以被派生类重写的方法所导致的。为了防止这个问题,构造函数应该只调用其自身类别中的final、static或private方法,因为这些方法不能被派生类重写。final类的构造函数可以调用任何它们自己的方法,因为(根据定义)它们不能从中派生。
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()
方法打印three
成员变量的当前值,该值为默认值0
(因为Test
实例尚未初始化)。printThree()
方法和Super()
构造函数各自退出,并初始化Test
实例(此时three
被设置为3
)。main()
方法再次调用printThree()
,这次打印出预期的值3
(因为Test
实例现在已经被初始化)。 Super
在 Test
之前初始化。然而,动态分派意味着(3)中的方法调用在未初始化的 Test
类的上下文中运行,导致意外行为。
this
禁止从构造函数传递this
到另一个对象的限制更容易解释一些。
基本上,一个对象直到其构造函数执行完毕后才被认为是完全初始化的(因为它的目的是完成对象的初始化)。因此,如果构造函数将对象的this
传递给另一个对象,则该另一个对象就拥有了对该对象的引用,即使该对象还没有完全初始化(因为它的构造函数仍在运行)。如果另一个对象尝试访问未初始化的成员或调用依赖于其完全初始化的原始对象的方法,则可能会出现意外行为。
关于如何产生意外行为的示例,请参阅this article。
Object
类之外的每个类都是派生类——我在这里使用“派生类”和“基类”这些术语来概述所讨论的特定类之间的关系。在构造函数中调用实例方法是危险的,因为对象尚未完全初始化(这主要适用于可以重写的方法)。此外,在构造函数中进行复杂处理已经被证明对测试性能有负面影响。
只要小心使用,使用可重写方法是不良的惯例。