为什么在Java中从构造函数调用方法被认为是不良实践?

25
在Java中,为什么从构造函数内部调用方法被认为是不良实践?如果该方法计算量很大,那么情况是否尤其严重?

2
从我的角度来看,这与对象可能处于“未初始化”状态有关,这意味着如果您调用的方法或其调用的子方法依赖于对象处于特定状态,它们可能会生成错误,或者构造函数的其余部分可能会更改这些状态。这些方法也可能被覆盖,从而以意想不到的方式更改对象的状态...我相信还有其他原因... - MadProgrammer
这里有一个例子展示了为什么这是不好的:https://dev59.com/VGMl5IYBdhLWcg3wyJUe - Rohit Jain
参见:http://www.ibm.com/developerworks/java/library/j-jtp0618/index.html - fadden
3个回答

33
首先,一般来说在构造函数中调用方法是没有问题的。问题主要出现在调用构造函数类的可重写方法以及将对象的"this"引用传递给其他对象的方法(包括构造函数)的特定情况。
避免可重写方法和"泄露this"的原因可能很复杂,但基本上都涉及防止使用未完全初始化的对象。
避免在构造函数中调用可重写方法的原因是由Java语言规范(JLS)§12.5中定义的实例创建过程所导致的。
在§12.5的处理过程中,当实例化一个派生类[1]时,其基类的初始化(即将其成员设置为初始值并执行其构造函数)会先于它自己的初始化发生。这旨在通过两个关键原则实现类的一致初始化:
  1. 每个类的初始化可以专注于仅初始化它明确声明的成员,因为所有从基类继承的其他成员已经被初始化。
  2. 每个类的初始化可以安全地使用其基类的成员作为其自己成员初始化的输入,因为保证它们在类初始化时已经正确初始化。

然而,有一个问题: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。本示例的事件顺序如下:
  1. main()方法中调用了new Test()
  2. 由于Test没有显式的构造函数,因此其超类(即Super())的默认构造函数被调用。
  3. Super()构造函数调用printThree()。这将分派到Test类中覆盖的方法。
  4. Test类的printThree()方法打印three成员变量的当前值,该值为默认值0(因为Test实例尚未初始化)。
  5. printThree()方法和Super()构造函数各自退出,并初始化Test实例(此时three被设置为3)。
  6. main()方法再次调用printThree(),这次打印出预期的值3(因为Test实例现在已经被初始化)。
如上所述,§12.5规定(2)必须在(5)之前发生,以确保 Super Test 之前初始化。然而,动态分派意味着(3)中的方法调用在未初始化的 Test 类的上下文中运行,导致意外行为。

避免泄露 this

禁止从构造函数传递this到另一个对象的限制更容易解释一些。

基本上,一个对象直到其构造函数执行完毕后才被认为是完全初始化的(因为它的目的是完成对象的初始化)。因此,如果构造函数将对象的this传递给另一个对象,则该另一个对象就拥有了对该对象的引用,即使该对象还没有完全初始化(因为它的构造函数仍在运行)。如果另一个对象尝试访问未初始化的成员或调用依赖于其完全初始化的原始对象的方法,则可能会出现意外行为。

关于如何产生意外行为的示例,请参阅this article


[1] 技术上讲,Java中除了Object类之外的每个类都是派生类——我在这里使用“派生类”和“基类”这些术语来概述所讨论的特定类之间的关系。
[2] 据我所知,JLS中没有给出为什么会出现这种情况的原因。另一种选择——在构造函数中禁止动态调度——会使整个问题无效,这可能正是C++不允许它的原因。


6
构造函数只应调用私有、静态或最终的方法。这有助于消除可能出现的重载问题。
此外,构造函数不应启动线程。在构造函数(或静态初始化程序)中启动线程存在两个问题:
  • 在非最终类中,它增加了与子类相关的问题的危险性
  • 它为允许该引用逃逸构造函数打开了大门
在构造函数(或静态初始化程序)中创建线程对象没有任何问题 - 只是不要在那里启动它。

我猜对象状态与声明为final的方法有关,能否请您更清楚地解释一下这些要点 @Luke Bigwood - Vamsi Pavan Mahesh
如果你想要深入了解关于'this'方法逃逸的解释,可以查看这个链接:http://www.ibm.com/developerworks/java/library/j-jtp0618/index.html 简而言之,如果在构造函数尚未完全完成时使用“this”,就可能发生该错误,并且与并发访问有关。 - Luke Bigwood
如果您对最佳实践感兴趣,此网站可能会为您提供一些宝贵的技巧。我个人认为这个格式非常简单和明确:http://www.javapractices.com/ - Luke Bigwood

4

在构造函数中调用实例方法是危险的,因为对象尚未完全初始化(这主要适用于可以重写的方法)。此外,在构造函数中进行复杂处理已经被证明对测试性能有负面影响。

只要小心使用,使用可重写方法是不良的惯例。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接