为什么从构造函数中调用方法被认为是不好的做法?
在Java中,为什么从构造函数中调用方法被认为是不好的做法? 如果这个方法计算量很大,那会特别糟糕吗?
首先,一般来说,在构造函数中调用方法没有问题。 这些问题特别针对调用构造函数类的可覆盖方法的特定情况,以及将对象的this
引用传递给其他对象的方法(包括构造函数)。
避免可覆盖方法和“泄漏”的原因可能很复杂,但它们基本上都与防止使用未完全初始化的对象有关。
避免调用可覆盖的方法
避免在构造函数中调用可覆盖方法的原因是Java语言规范(JLS)第12.5节中定义的实例创建过程的结果。
除此之外,§12.5的过程确保在实例化派生类[1]时 ,其基类的初始化(即将其成员设置为其初始值并执行其构造函数)在其自己的初始化之前发生。 这旨在通过两个关键原则允许类的一致初始化:
- 每个类的初始化可以专注于初始化它显式声明自己的成员,安全地知道从基类inheritance的所有其他成员都已经初始化。
- 每个类的初始化可以安全地使用其基类的成员作为其自己成员初始化的输入,因为它保证在类初始化发生时已经正确初始化。
但是,有一个问题:Java允许构造函数中的动态调度[2] 。 这意味着如果作为派生类实例化的一部分执行的基类构造函数调用派生类中存在的方法,则在该派生类的上下文中调用它。
所有这一切的直接后果是,在实例化派生类时,在初始化派生类之前调用基类构造函数。 如果该构造函数调用被派生类重写的方法,则它是被调用的派生类方法(不是基类方法), 即使派生类尚未初始化 。 显然,如果该方法使用派生类的任何成员,则这是一个问题,因为它们尚未初始化。
显然,问题是基类构造函数调用方法的结果,这些方法可以被派生类覆盖。 为了防止这个问题,构造函数应该只调用自己的类的方法,这些方法是final,static或private,因为这些方法不能被派生类覆盖。 最终类的构造函数可以调用它们的任何方法,因为(根据定义)它们不能从中派生出来。
JLS的示例12.5-2很好地certificate了这个问题:
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
实例现已初始化)。
如上所述,§12.5规定(2)必须在(5)之前发生,以确保在Test
之前初始化Super
。 但是,动态分派意味着(3)中的方法调用在未初始化的Test
类的上下文中运行,从而导致意外行为。
避免泄漏this
this
从构造函数传递到另一个对象的限制更容易解释。
基本上,在构造函数完成执行之前,不能将对象视为完全初始化(因为其目的是完成对象的初始化)。 因此,如果构造函数将对象的this
传递给另一个对象,那么另一个对象就会引用该对象,即使它尚未完全初始化(因为它的构造函数仍在运行)。 如果另一个对象然后尝试访问未初始化的成员或调用依赖于其完全初始化的原始对象的方法,则可能导致意外行为。
有关如何导致意外行为的示例,请参阅此文章 。
[1]从技术上讲,除了Object
之外,Java中的每个类都是派生类 – 我只是在这里使用术语“派生类”和“基类”来概述所讨论的特定类之间的关系。
[2]在JLS中没有理由(据我所知)为什么会出现这种情况。 替代方案 – 不允许构造函数中的动态调度 – 会使整个问题没有实际意义,这可能正是C ++不允许的原因。
构造函数应该只调用private,static或final方法。 这有助于摆脱Overriding可能出现的问题。
此外,构造函数不应该启动线程。 在构造函数(或静态初始化程序)中启动线程有两个问题:
- 在非final类中,它增加了子类问题的危险
- 它打开了允许此引用转义构造函数的大门
在构造函数(或静态初始化程序)中创建线程对象没有任何问题 – 只是不要在那里启动它。
在构造函数中调用实例方法是危险的,因为对象尚未完全初始化(这主要适用于可被覆盖的方法)。 此外,已知构造函数中的复杂处理对测试能力具有负面影响。
在做的时候要小心,不好的做法是使用覆盖方法来做到这一点。