【问题标题】:Why is it considered bad practice in Java to call a method from within a constructor? [duplicate]为什么在 Java 中从构造函数中调用方法被认为是不好的做法? [复制]
【发布时间】:2013-08-23 07:34:33
【问题描述】:

在 Java 中,为什么从构造函数中调用方法被认为是不好的做法?如果方法计算量大,会不会特别糟糕?

【问题讨论】:

  • 在我的脑海中,它与对象处于“未初始化”状态的可能性有关,这意味着如果您正在调用的方法或它调用的子方法,请依赖如果对象处于特定状态,它们可能会产生错误,或者构造函数的其余部分可能会改变这些状态。这些方法也可能被覆盖,再次以意想不到的方式改变对象的状态......我确定这是其他原因......
  • Here's one example 说明它为什么不好。

标签: java


【解决方案1】:

首先,在构造函数中调用方法通常没有问题。问题特别在于调用构造函数类的可覆盖方法以及将对象的 this 引用传递给其他对象的方法(包括构造函数)的特殊情况。

避免覆盖方法和“泄漏this”的原因可能很复杂,但它们基本上都与防止使用未完全初始化的对象有关。

避免调用可覆盖的方法

避免在构造函数中调用可覆盖方法的原因是 Java 语言规范 (JLS) 的 §12.5 中定义的实例创建过程的结果。

除其他事项外,§12.5 的过程确保在实例化派生类[1] 时,其基类的初始化(即,将其成员设置为其初始值并执行其构造函数) 发生在它自己的初始化之前。这旨在通过两个关键原则允许一致地初始化类:

  1. 每个类的初始化可以专注于仅初始化它明确声明的成员,确保从基类继承的所有其他成员都已初始化。
  2. 每个类的初始化都可以安全地使用其基类的成员作为其自身成员初始化的输入,因为可以保证它们在类初始化时已正确初始化。

但是,有一个问题:Java 允许在构造函数中进行动态调度[2]。这意味着,如果作为派生类实例化的一部分执行的基类构造函数调用派生类中存在的方法,则会在该派生类的上下文中调用它。

所有这一切的直接后果是,在实例化派生类时,会在派生类初始化之前调用基类构造函数。如果该构造函数调用了被派生类覆盖的方法,则调用的是派生类方法(不是基类方法),即使派生类尚未初始化。如果该方法使用派生类的任何成员,显然这是一个问题,因为它们还没有被初始化。

显然,问题是基类构造函数调用了可以被派生类覆盖的方法的结果。为了防止这个问题,构造函数应该只调用它们自己类的最终、静态或私有方法,因为这些方法不能被派生类覆盖。 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. new Test()main() 方法中被调用。
  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) 之前发生,以确保 SuperTest 之前初始化。但是,动态分派意味着(3)中的方法调用是在未初始化的Test 类的上下文中运行的,从而导致意外行为。

避免泄漏this

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

基本上,一个对象在其构造函数完成执行之前不能被认为是完全初始化的(因为它的目的是完成对象的初始化)。因此,如果构造函数将对象的this 传递给另一个对象,那么即使它尚未完全初始化(因为它的构造函数仍在运行),该对象也会引用该对象。如果另一个对象随后尝试访问未初始化的成员或调用依赖于它被完全初始化的原始对象的方法,则可能会导致意外行为。

有关这如何导致意外行为的示例,请参阅this article


[1] 从技术上讲,Java 中除Object 之外的每个类都是派生类——我在这里仅使用术语“派生类”和“基类”来概述相关特定类之间的关系。
[2] JLS(据我所知)没有说明为什么会出现这种情况。另一种选择——在构造函数中不允许动态分派——会使整个问题变得毫无意义,这可能正是 C++ 不允许它的原因。

【讨论】:

    【解决方案2】:

    构造函数只能调用私有、静态或最终的方法。这有助于消除覆盖可能出现的问题。

    另外,构造函数不应该启动线程。在构造函数(或静态初始化程序)中启动线程有两个问题:

    • 在非最终类中,它增加了子类出现问题的危险
    • 它打开了允许 this 引用逃离构造函数的大门

    在构造函数(或静态初始化程序)中创建线程对象没有任何问题 - 只是不要从那里开始。

    【讨论】:

    • 我猜对象状态必须与声明为 final 的方法有关,您能否更清楚地解释要点@Luke Bigwood
    • 如果你想深入解释'this'方法转义,你可以查看这个链接:ibm.com/developerworks/java/library/j-jtp0618/index.html 简而言之,如果你在构造函数没有'时使用'this'可能会发生错误t 完全完成,与并发访问有关。
    • 如果您对最佳实践感兴趣,本网站可能会为您提供一些有价值的提示,我发现格式非常简单且定义明确:javapractices.com
    【解决方案3】:

    在构造函数中调用实例方法是危险的,因为对象还没有完全初始化(这主要适用于不能被覆盖的方法)。众所周知,构造函数中的复杂处理会对测试能力产生负面影响。

    在做的时候要小心,用可覆盖的方法来做是不好的做法。

    【讨论】:

      猜你喜欢
      • 2012-02-01
      • 2010-10-22
      • 2011-11-20
      • 1970-01-01
      • 1970-01-01
      • 2011-12-01
      • 2012-06-01
      • 1970-01-01
      相关资源
      最近更新 更多