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

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

问题描述

在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?

推荐答案

首先,通常在构造函数中调用方法没有问题.这些问题特别涉及以下特定情况:调用构造函数的类的可重写方法,并将对象的this引用传递给其他对象的方法(包括构造函数).

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.

避免在构造函数中调用可重写方法的原因是

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] 时,对其基类进行初始化(即,将其成员设置为其初始值并执行其构造函数) )在其自身初始化之前发生.这旨在通过两个关键原则来实现类的一致初始化:

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.

很明显,此问题是基类构造函数调用方法的结果,该方法可以被派生类覆盖.为防止此问题,构造函数应仅调用自己的最终,静态或私有类的方法,因为这些方法不能被派生类覆盖.最终类的构造函数可以调用其任何方法,因为(根据定义)它们不能从其派生.

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:

    main()方法中调用
  1. new Test().
  2. 由于Test没有显式构造函数,因此将调用其超类的默认构造函数(即Super()).
  3. Super()构造函数调用printThree().这将分派到Test类中方法的重写版本.
  4. Test类的printThree()方法将打印three成员变量的当前值,这是默认值0(因为Test实例尚未初始化)./li>
  5. printThree()方法和Super()构造函数各自退出,并且Test实例被初始化(此时three设置为3).
  6. main()方法再次调用printThree(),这一次将打印3的期望值(因为Test实例已被初始化).
  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)之前发生,以确保SuperTest之前被初始化.但是,动态分派意味着(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从构造函数传递到另一个对象的限制更容易解释.

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.

有关如何导致意外行为的示例,请参考本文.

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.

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

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