为什么在 Java 中从构造函数中调用方法被认为是不好的做法?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/18348797/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Why is it considered bad practice in Java to call a method from within a constructor?
提问by Cricketer
In Java, why is it considered bad practice to call a method from within a constructor? Is it especially bad if the method is computationally heavy?
在 Java 中,为什么从构造函数中调用方法被认为是不好的做法?如果该方法计算量很大,是否特别糟糕?
采纳答案by Mac
First, in general there's no problem with calling methods in a constructor. The issues are specifically with the particular cases of calling overridable methods of the constructor's class, and of passing the object's this
reference to methods (including constructors) of other objects.
首先,通常在构造函数中调用方法没有问题。这些问题特别与调用构造函数类的可覆盖方法以及将对象的this
引用传递给其他对象的方法(包括构造函数)的特定情况有关。
The reasons for avoiding overridable methods and "leaking this
" can be complicated, but they basically are all concerned with preventing use of incompletely initialised objects.
避免可覆盖方法和“泄漏this
”的原因可能很复杂,但它们基本上都与防止使用未完全初始化的对象有关。
Avoid calling overridable methods
避免调用可覆盖的方法
The reasons for avoiding calling overridable methods in constructors are a consequence of the instance creation process defined in §12.5of the Java Language Specification (JLS).
避免在构造函数中调用可覆盖方法的原因是 Java 语言规范 (JLS) §12.5中定义的实例创建过程的结果。
Among other things, the process of §12.5 ensures that when instantiating a derived class[1], the initialisation of its base class (i.e. setting its members to their initial values and execution of its constructor) occurs before its own initialisation. This is intended to allow consistent initialisation of classes, through two key principles:
除其他外,§12.5 的过程确保在实例化派生类[1] 时,其基类的初始化(即将其成员设置为其初始值并执行其构造函数)发生在其自身的初始化之前。这旨在通过两个关键原则允许类的一致初始化:
- The initialisation of each class can focus on initialising only the members it explicitly declares itself, safe in the knowledge that all other members inherited from the base class have already been initialised.
- The initialisation of each class can safely use members of its base class as inputs to the initialisation of its own members, as it is guaranteed they've been properly initialised by the time the initialisation of the class occurs.
- 每个类的初始化可以专注于仅初始化它显式声明自己的成员,知道从基类继承的所有其他成员已经初始化是安全的。
- 每个类的初始化都可以安全地使用其基类的成员作为其自身成员初始化的输入,因为可以保证在类初始化发生时它们已经正确初始化。
There is, however, a catch: Java allows dynamic dispatch in constructors[2]. This means that if a base class constructor executing as part of the instantiation of a derived class calls a method that exists in the derived class, it is called in the context of that derived class.
然而,有一个问题:Java 允许在构造函数中进行动态调度[2]。这意味着,如果作为派生类实例化的一部分执行的基类构造函数调用派生类中存在的方法,则在派生类的上下文中调用它。
The direct consequence of all of this is that when instantiating a derived class, the base class constructor is called before the derived class is initialised. If that constructor makes a call to a method that is overridden by the derived class, it is the derived class method (not the base class method) that is called, even though the derived class has not yet been initialised. Evidently this is a problem if that method uses any members of the derived class, since they haven't been initialised yet.
所有这一切的直接结果是,在实例化派生类时,基类构造函数在派生类初始化之前被调用。如果该构造函数调用被派生类覆盖的方法,则调用的是派生类方法(而不是基类方法),即使派生类尚未初始化。显然,如果该方法使用派生类的任何成员,这是一个问题,因为它们尚未初始化。
Clearly, the issue is a result of the base class constructor calling methods that can be overriden by the derived class. To prevent the issue, constructors should only call methods of their own class that are final, static or private, as these methods cannot be overridden by derived classes. Constructors of final classes may call any of their methods, as (by definition) they cannot be derived from.
显然,问题是基类构造函数调用的方法可以被派生类覆盖的结果。为了防止这个问题,构造函数应该只调用他们自己类的最终、静态或私有的方法,因为这些方法不能被派生类覆盖。final 类的构造函数可以调用它们的任何方法,因为(根据定义)它们不能派生自。
Example 12.5-2of the JLS is a good demonstration of this issue:
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();
}
}
This program prints 0
then 3
. The sequence of events in this example is as follows:
该程序打印0
then 3
。本例中的事件顺序如下:
new Test()
is called in themain()
method.- Since
Test
has no explicit constructor, the default constructor of its superclass (namelySuper()
) is called. - The
Super()
constructor callsprintThree()
. This is dispatched to the overriden version of the method in theTest
class. - The
printThree()
method of theTest
class prints the current value of thethree
member variable, which is the default value0
(since theTest
instance hasn't been initialised yet). - The
printThree()
method andSuper()
constructor each exit, and theTest
instance is initialised (at which pointthree
is then set to3
). - The
main()
method callsprintThree()
again, which this time prints the expected value of3
(since theTest
instance has now been initialised).
new Test()
在main()
方法中调用。- 由于
Test
没有显式构造函数,因此Super()
调用其超类(即)的默认构造函数。 - 该
Super()
构造函数调用printThree()
。这被分派到Test
类中方法的覆盖版本。 - 类的
printThree()
方法Test
打印three
成员变量的当前值,这是默认值0
(因为Test
实例尚未初始化)。 - 的
printThree()
方法和Super()
构造每个出口,和Test
实例被初始化(在该点处three
的一个设置到3
)。 - 该
main()
方法printThree()
再次调用,这一次打印预期值3
(因为Test
实例现在已经初始化)。
As described above, §12.5 states that (2) must happen before (5), to ensure that Super
is initialised before Test
is. However, dynamic dispatch means that the method call in (3) is run in the context of the uninitialised Test
class, leading to the unexpected behaviour.
如上所述,§12.5 规定 (2) 必须在 (5) 之前发生,以确保在Super
is 之前初始化Test
。但是,动态分派意味着 (3) 中的方法调用在未初始化的Test
类的上下文中运行,从而导致意外行为。
Avoid leaking this
避免泄漏 this
The restriction against passing this
from a constructor to another object is a little easier to explain.
禁止this
从构造函数传递到另一个对象的限制更容易解释。
Basically, an object cannot be considered fully initialised until its constructor has completed execution (since its purpose is to complete the initialisation of the object). So, if the constructor passes the object's this
to another object, that other object then has a reference to the object even though it hasn't been fully initialised (since its constructor is still running). If the other object then attempts to access an uninitialised member or call a method of the original object that relies on it being fully initialised, unexpected behaviour is likely to result.
基本上,在其构造函数完成执行之前,不能认为对象已完全初始化(因为它的目的是完成对象的初始化)。因此,如果构造函数将对象传递this
给另一个对象,那么即使该对象尚未完全初始化(因为它的构造函数仍在运行),该对象仍具有对该对象的引用。如果另一个对象随后尝试访问未初始化的成员或调用依赖于它完全初始化的原始对象的方法,则可能会导致意外行为。
For an example of how this can result in unexpected behaviour, please refer to this article.
有关这如何导致意外行为的示例,请参阅本文。
[1] 从技术上讲,除了
Object
Object
派生类之外,Java 中的每个类都是派生类——我在这里只使用术语“派生类”和“基类”来概述所讨论的特定类之间的关系。[2] JLS 中(据我所知)没有说明为什么会这样。替代方案 - 禁止在构造函数中进行动态调度 - 会使整个问题变得毫无意义,这可能正是 C++ 不允许这样做的原因。
回答by Joban Dhillon
Calling instance method in constructor is dangerous as the object is not yet fully initialized (this applies mainly to methods than can be overridden). Also complex processing in constructor is known to have a negative impact on test-ability.
在构造函数中调用实例方法是危险的,因为对象尚未完全初始化(这主要适用于可以覆盖的方法)。众所周知,构造函数中的复杂处理会对可测试性产生负面影响。
Just be careful when doing, its bad practice to do it with override able methods.
做的时候要小心,使用可覆盖的方法来做是不好的做法。
回答by Luke Bigwood
Constructors should only ever call methods that are private, static or final. This helps get rid of the issues that can appear with Overriding.
构造函数应该只调用私有的、静态的或最终的方法。这有助于摆脱覆盖可能出现的问题。
Also, Constructors shouldn't start threads. There are two problems with starting a thread in a constructor (or static initializer):
此外,构造函数不应启动线程。在构造函数(或静态初始值设定项)中启动线程有两个问题:
- in a non-final class, it increases the danger of problems with subclasses
- it opens the door for allowing the this reference to escape the constructor
- 在非最终类中,它增加了子类出现问题的危险
- 它打开了允许 this 引用逃脱构造函数的大门
There's nothing wrong with creating a thread object in a constructor (or static initializer) - just don't start it there.
在构造函数(或静态初始值设定项)中创建线程对象并没有错——只是不要在那里启动它。