为什么从构造函数中调用方法被认为是不好的做法?

Cri*_*ter 22 java

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

Mac*_*Mac 29

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

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

避免调用可覆盖的方法

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

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

  1. 每个类的初始化可以专注于初始化它显式声明自己的成员,安全地知道从基类继承的所有其他成员已经初始化.
  2. 每个类的初始化可以安全地使用其基类的成员作为其自己成员初始化的输入,因为它保证在类的初始化发生时它们已被正确初始化.

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

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

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

0然后打印该程序3.此示例中的事件序列如下:

  1. new Test()在main()方法中被调用.
  2. 由于Test没有显式构造函数,因此Super()调用其超类(即)的默认构造函数.
  3. 该Super()构造函数调用printThree().这将被分派到Test类中方法的重写版本.
  4. 类的printThree()方法Test打印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给另一个对象,那么该另一个对象就会引用该对象,即使它尚未完全初始化(因为它的构造函数仍在运行).如果另一个对象然后尝试访问未初始化的成员或调用依赖于其完全初始化的原始对象的方法,则可能导致意外行为.

有关如何导致意外行为的示例,请参阅此文章.


[1]从技术上讲,Java中的每个类Object都是派生类 - 我只是在这里使用术语"派生类"和"基类"来概述所讨论的特定类之间的关系.
[2]在JLS中没有理由(据我所知)为什么会这样.替代方案 - 不允许构造函数中的动态调度 - 会使整个问题变得没有问题,这可能正是C++不允许的原因.


Luk*_*ood 6

构造函数应该只调用private,static或final方法.这有助于摆脱Overriding可能出现的问题.

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

  • 在非final类中,它增加了子类问题的危险
  • 它打开了允许此引用转义构造函数的大门

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