当前位置: 首页 > 面试题库 >

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

舒博雅
2023-03-14
问题内容

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


问题答案:

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

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

避免调用可覆盖的方法

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

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

  1. 每个类的初始化都可以专注于仅初始化它明确声明的成员,这是安全的,因为要知道从基类继承的所有其他成员都已被初始化。
  2. 每个类的初始化都可以安全地使用其基类的成员作为其自身成员的初始化的输入,因为可以确保在初始化该类时已对其进行了正确的初始化。

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

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

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

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();
    }
}

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
++不允许这样做的原因。



 类似资料:
  • 我有一个问题,关于什么是正确的做法,使用SwingU实用程序的调用稍后方法。 所以首先,我想确认我理解正确。 据我所知,对GUI的更改必须在EDT上完成,因为Swing组件不是线程安全的。invokeLater方法将Runnable作为参数,该Runnable中包含的任何内容都将在EDT上运行。因此,对Swing组件的任何调用都被放入一种队列中,在EDT上一次执行一个。 有了这些,我的问题是:使用

  • 问题内容: 是仅在继承还是大多数情况下才是正确的? 问题答案: 这是真的。 因为二传手总是方法。如果您的课程不是,那么就会出现外来方法调用的问题。这不是线程安全的,即称为转义引用。因此,从构造函数中,如果要调用方法,则应为或。其他对象将不会发生,这会在实际系统中导致许多错误。 除了上述内容之外,我们永远不要从中调用method,因为 如果该类旨在继承,则Constructor不得直接或间接调用可重

  • 问题内容: 编辑:在上面的链接中回答的问题。 不,PHP中的“全局”与其他语言中的“全局”不同,尽管它不引入任何安全性问题,但会使其他人较难理解该代码。 OP: 项目摘要-我正在编写一个Web CMS,以使自己熟悉PHP / MySQL。为了分解代码,我对以下基本层/模块有一个概念: 数据 -MySQL表 -PHP变量 功能 -SQL- 获取/设置/等 -前端-显示页面 -后端-管理器 演示文稿

  • 问题内容: 我有一个类的以下代码,它工作正常: 但是,我认为在该方法中实现OnClickListener是一种不好的做法。为什么会有这种不好的做法,还有什么更好的选择? 问题答案: 最好在ViewHolder中处理单击逻辑的原因是,它允许更明确的单击侦听器。如Commonsware书中所述: 很久以来,ListView行中的可单击小部件(如RatingBar)一直与行本身的单击事件发生冲突。获取可

  • 问题内容: 在回答了有关如何使用强制释放Java中的对象(那个家伙正在清除1.5GB的HashMap)的问题后,有人告诉我手动调用是一种不好的做法,但是注释并不完全令人信服。此外,似乎没有人敢于赞成,也没有反对我的答案。 有人告诉我这是一种不好的做法,但是后来我又被告知,垃圾收集器的运行不再系统地停止世界,而且它也可以有效地被JVM用作提示,所以我有点不知所措。 我确实知道,JVM在需要回收内存时

  • 问题内容: Java 1.5 的Java序列化规范说: 对于可序列化的对象,运行第一个不可序列化超类型的no- arg构造函数。对于可序列化的类,将字段初始化为适合其类型的默认值。然后,通过调用特定于类的readObject方法来恢复每个类的字段,如果未定义这些字段,则通过调用defaultReadObject方法来恢复它们。请注意,在反序列化期间,不会为可序列化的类执行字段初始化程序和构造函数。