【问题标题】:Java - (Anonymous subclass) overriding method during object instance constructionJava - (匿名子类)对象实例构造期间的覆盖方法
【发布时间】:2017-10-08 04:34:46
【问题描述】:

我正在维护一些如下所示的 Java 8 代码:

Class Entity  {
   protected Model theModel;

   public Entity()  {
       init();
   }

   protected void init()  {
       this.theModel = new Model();
   }
}

Class Model  {
}

Class SubModel extends Model {
}

main {
    Entity newEntity = new Entity()  {
        @Override
        protected void init()  {
            this.theModel = new SubModel();
        }
    };
}

代码目前可以正确编译和运行,但我现在需要更新它。

我的问题是:

  1. newEntity 的构造过程中,init() 方法的覆盖是如何工作的?
  2. 对象构造函数语句中包含的此方法覆盖的正确术语是什么?

到目前为止,我的研究表明 Java 不能动态覆盖方法 - 不能在此基础上进行覆盖,因为方法覆盖是针对每个类而不是针对每个对象的。但是这段代码sn-p似乎表明Java在实践中可以做到?


更新:请注意,在 main 中创建 newEntity 会创建一个匿名子类,并且仅针对该匿名子类覆盖 init() 方法。这在下面的两个优秀答案中得到了更好的解释。

【问题讨论】:

  • 请阅读this question 和答案。这是关于转义 this 参考。

标签: java inheritance constructor java-8 polymorphism


【解决方案1】:

据我所知,这里没有什么特别之处,只是经典的 constructor chaining 和应用于虚拟方法调用的多态性。

当你实例化你的匿名类时,它会自动调用它的default constructor(由编译器自动给出),在它的默认构造函数成功之前,它必须首先调用它的父类默认构造函数,然后它会调用@ 987654324@ 方法,因为它已被您的匿名类覆盖,多态性地最终调用子类中的 init 方法,该方法将模型初始化为您的 SubModel 实例。

Joshua Bloch 在他的著名著作Effective Java 中对这种模式提出了一些有趣的论据,在“第 17 项:设计和文档以继承或禁止”部分中,他写道:

“一个类必须遵守更多的限制才能允许 遗产。构造函数不得调用可覆盖的方法, 直接或间接。如果您违反此规则,程序将失败 结果。超类构造函数在子类之前运行 构造函数,因此子类中的覆盖方法将被调用 在子类构造函数运行之前。如果覆盖方法 取决于子类构造函数执行的任何初始化, 该方法将不会按预期运行。为了具体化,这里是 违反此规则的类:”

他接着举了一个你可以好好学习的例子:

“这是一个覆盖overrideMe的子类,它是 被Super 的唯一构造函数错误地调用:”

public class Super {
    // Broken - constructor invokes an overridable method
    public Super() {
        overrideMe();
    }

    public void overrideMe() {
    }
}

public final class Sub extends Super {
    private final Date date; // Blank final, set by constructor

    Sub() {
        date = new Date();
    }

    // Overriding method invoked by superclass constructor
    @Override public void overrideMe() {
        System.out.println(date);
    }

    public static void main(String[] args) {
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

“你可能希望这个程序打印出两次日期,但它 第一次打印出 null,因为 overrideMe 方法是 在 Sub 构造函数之前由 Super 构造函数调用 初始化日期字段的机会。请注意,该程序观察到 两个不同州的决赛场!另请注意,如果 overrideMe 有 调用date 上的任何方法,调用将抛出一个 NullPointerExceptionSuper 构造函数调用overrideMe 时。 该程序不抛出NullPointerException 的唯一原因是 它代表println方法有特殊规定 处理一个空参数。”

因此,正如您所看到的,正如 Joshua Bloch 解释得很好,风险潜伏在阴影中:在您可以在被覆盖的方法中执行的操作中,您有权访问构造函数链中的实例变量还没有机会初始化。关键是在构造函数链完全初始化对象状态之前,不应允许您触摸对象状态。

您可能会说,在您的特定情况下,这不会发生,因为您没有非法更改状态,并且您的覆盖方法受到保护,而不是公开,但问题是任何接触此代码的人都需要非常清楚地了解所有这些事情发生在幕后,发生在您当前代码以外的地方。在维护期间很容易犯一个严重的错误,特别是当你或其他一些开发人员回到这里进行更改时,可能是在最初定义之后几个月甚至几年,并且失去了所有这些危险的上下文,有人引入了一个错误,真的很难找到和修复。

【讨论】:

  • 感谢您的出色回答。体现了 StackOverflow 的所有优点。
【解决方案2】:

如果真的和你展示的完全一样,而且没有明显的图片缺失,那么你要维护的代码就是坏的,坏代码的维护是很麻烦的。

在构造函数中调用可覆盖是合法的,但这是非常糟糕的做法,因为可覆盖将在尚未调用构造函数的后代上调用,这是灾难性的。在琐碎的例子中,后代有空的构造函数可能无关紧要,但是当事情变得更复杂时,后代突然需要有一个非空的构造函数,这势必会造成很大的麻烦。

随着时间的推移,事情确实会变得更加复杂。

一个还不错的 IDE 会在从构造函数中调用可覆盖对象时发出一个大警告。这反过来意味着编写代码时启用的警告数量不足,这可能意味着它充满了此类问题。

对象构造函数中包含的此方法覆盖的正确术语是:错误

如果不进行一些重大重构,您将无法纠正此问题。要么模型需要作为构造函数参数传递,要么构造函数必须忍受在构造过程中模型根本不知道的事实。

您关于“动态”覆盖方法的问题有点奇怪,并且可能不必要地使事情复杂化。虚拟方法分派是通过虚拟方法表在内部完成的。每个类都有自己的虚拟方法表,它永远不会改变。但是,当构造函数执行时,this 指针指向实际(后代)实例,因此有效的虚拟方法表是后代的。因此,当构造函数调用一个可重写对象时,将调用其后代的可重写对象。

这与 C++ 不同,在 C++ 中,在构造时有效的虚拟方法表是声明构造函数的类的虚拟方法表,(无论它是否已被子类化),因此当您从内部调用虚拟方法时您没有调用任何覆盖方法的 C++ 构造函数。

【讨论】:

  • 感谢您提供这个非常有帮助的答案。我现在明白我的术语是错误的,这不是动态覆盖:它是一个匿名子类。我将研究如何在不破坏当前功能的情况下改进此代码...
猜你喜欢
  • 1970-01-01
  • 2011-04-21
  • 1970-01-01
  • 2023-03-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多