Java - (匿名子类)在对象实例构造期间重写方法

rad*_*ast 7 java polymorphism inheritance constructor java-8

我正在维护一些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();
        }
    };
}
Run Code Online (Sandbox Code Playgroud)

代码目前正在编译并正确运行,但我现在需要更新它.

我的问题是:

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

到目前为止,我的研究表明Java无法动态覆盖方法 - 在此基础上无法覆盖,因为方法覆盖是按类而不是按对象.但是这段代码片段似乎表明Java可以在实践中做到这一点?

Edw*_*rzo 9

据我所知,这里没有什么特别的,只是经典的构造函数链接和多态应用于虚方法调用.

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

约书亚布洛赫在其着名的书" 有效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();
    }
}
Run Code Online (Sandbox Code Playgroud)

"你可能希望这个程序打印两次日期,但它第一次输出为null,因为在overrideMe构造Sub函数有机会初始化日期字段之前,超级构造函数会调用该方法.请注意,此程序会观察两个不同状态的最终字段!还需要注意的是,如果overrideMe援引任何方法date,调用会抛出 NullPointerException的时候Super构造函数调用overrideMe.这个程序没有按NullPointerException原样抛出的唯一原因是该println方法具有处理null参数的特殊规定."

所以,正如你所看到的,并且正如Joshua Bloch所解释的那样,风险隐藏在阴影中:在被覆盖的方法中你可以做什么的可能性,你有许可触摸构造链还没有的实例变量有机会初始化.关键是在构造函数链完全初始化之前,不应该允许您触摸对象状态.

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

  • 谢谢你这个出色的答案.举例说明StackOverflow的所有优点. (6认同)

Mik*_*kis 5

如果它实际上与您向我们展示的完全一样,并且图片中没有重要部分缺失,那么您必须维护的代码很糟糕,并且维护错误的代码非常麻烦.

在构造函数中调用overridable是合法的,但这是非常糟糕的做法,因为可以在尚未调用构造函数的后代上调用overridable,这是灾难性的.在简单的例子中,后代有空构造函数可能并不重要,但是当事情变得更复杂时,它必然会导致重大问题,并且突然有一天后代需要有一个非空的构造函数.

随着时间的推移,事情往往变得更加复杂.

一个体面的IDE会在构造函数中调用overridable时发出一个很大的警告.这反过来意味着代码编写时启用的警告数量不足,这可能意味着它充满了这种问题.

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

没有一些重大的重构,你无法纠正这个问题.模型需要作为构造函数参数传递,或者构造函数必须承受在构造期间根本无法知道模型的事实.

关于"动态"覆盖方法的问题有点奇怪,可能会使事情变得不必要地复杂化.虚拟方法调度通过虚拟方法表在内部完成.每个类都有自己的虚方法表,永远不会改变.但是,当构造函数执行时,this指针指向实际(后代)实例,因此有效的虚拟方法表是后代的虚拟方法表.因此,当构造函数调用overridable时,将调用后代的可覆盖.

这与C++不同,其中构造时生效的虚方法表是声明构造函数的类的虚方法表(不管它是否已经被子类化),所以当你从C++构造函数中调用虚方法时没有调用任何重写方法.

  • 谢谢你这个非常有用的答案.我现在明白我的术语是错误的,这不是动态覆盖:它是一个匿名子类.我将看看如何在不破坏当前功能的情况下改进此代码... (2认同)