为什么从构造函数中调用方法被认为是不好的做法? [英] Why is it considered bad practice to call a method from within a constructor?

查看:135
本文介绍了为什么从构造函数中调用方法被认为是不好的做法?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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?

推荐答案

首先,一般来说,在构造函数中调用方法没有问题。这些问题特别针对调用构造函数类的可覆盖方法的特定情况,以及将对象的引用传递给其他对象的方法(包括构造函数)。

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.

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

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.

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

The reasons for avoiding calling overridable methods in constructors are a consequence of the instance creation process defined in §12.5 of the Java Language Specification (JLS).

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

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:


  1. 每个类的初始化可以专注于仅初始化成员显式声明自己,安全地知道从基类继承的所有其他成员都已经初始化。

  2. 每个类的初始化可以安全地使用其基类的成员作为输入初始化自己的成员,因为它保证在课程初始化时已经正确初始化。

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

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.

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

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.

显然,问题是基础的结果类构造函数调用可以由派生类重写的方法。为了防止这个问题,构造函数应该只调用自己的类的方法,这些方法是final,static或private,因为这些方法不能被派生类覆盖。最终类的构造函数可以调用它们的任何方法,因为(根据定义)它们不能从中派生。

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.

示例12.5-2 JLS很好地证明了这个问题:

Example 12.5-2 of the JLS is a good demonstration of this issue:

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 。此示例中的事件序列如下:

This program prints 0 then 3. The sequence of events in this example is as follows:


  1. new Test() main()方法中调用。

  2. 由于测试没有显式构造函数,其超类的默认构造函数(即 Super())被调用。

  3. 超级()构造函数调用 printThree()。这将被分派到 Test 类中方法的覆盖版本。

  4. printThree() Test 类的方法打印成员变量的当前值,即默认值 0 (因为测试实例尚未初始化)。

  5. 每个出口的 printThree()方法和 Super()构造函数,以及测试实例初始化(此时然后设置为 3 )。

  6. main()方法再次调用 printThree(),这次打印预期值 3 (因为测试实例现已初始化)。

  1. new Test() is called in the main() method.
  2. Since Test has no explicit constructor, the default constructor of its superclass (namely Super()) is called.
  3. The Super() constructor calls printThree(). This is dispatched to the overriden version of the method in the Test class.
  4. The printThree() method of the Test class prints the current value of the three member variable, which is the default value 0 (since the Test instance hasn't been initialised yet).
  5. The printThree() method and Super() constructor each exit, and the Test instance is initialised (at which point three is then set to 3).
  6. The main() method calls printThree() again, which this time prints the expected value of 3 (since the Test instance has now been initialised).

如上所述,§12.5规定(2)必须在(5)之前发生,以确保超级测试之前初始化。但是,动态分派意味着(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.

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

基本上,在构造函数完成执行之前,不能将对象视为完全初始化(因为它的目的是完成对象的初始化)。因此,如果构造函数将对象的 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.

For一个如何导致意外行为的示例,请参阅本文

For an example of how this can result in unexpected behaviour, please refer to this article.



[1]从技术上讲,Java中的每个类除了 Object 是一个派生类 - 我只是在这里使用术语'派生类'和'基类'来概述所讨论的特定类之间的关系。

[2]没有理由给出在JLS(据我所知),为什么会这样。替代方案 - 不允许构造函数中的动态调度 - 会使整个问题没有实际意义,这可能正是C ++不允许它的原因。


[1] Technically, every class in Java except Object is a derived class - I just use the terms 'derived class' and 'base class' here to outline the relationship between the particular classes in question.
[2] There's no reason given in the JLS (as far as I'm aware) as to why this is the case. The alternative - disallowing dynamic dispatch in constructors - would make the whole issue moot, which is probably exactly why C++ doesn't allow it.

这篇关于为什么从构造函数中调用方法被认为是不好的做法?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆