当Base类构造函数在Java中调用重写方法时,Derived类对象的状态

请参考下面的Java代码:

class Base{ Base(){ System.out.println("Base Constructor"); method(); } void method(){} } class Derived extends Base{ int var = 2; Derived(){ System.out.println("Derived Constructor"); } @Override void method(){ System.out.println("var = "+var); } } class Test2{ public static void main(String[] args) { Derived b = new Derived(); } } 

看到的输出是:

 Base Constructor var = 0 Derived Constructor 

我认为var = 0的发生是因为Derived对象是半初始化的; 类似于Jon Skeet在这里所说的

我的问题是:

如果尚未创建Derived类对象,为什么会调用重写的方法?

在什么时间点,var赋值为0?

是否存在需要此类行为的用例?

  • Derived对象已经创建 – 只是构造函数尚未运行。 在创建它之后,对象的类型永远不会在Java中更改,这在所有构造函数运行之前发生。

  • 在构造函数运行之前, var被赋予默认值0作为创建对象的过程的一部分。 基本上,类型引用被设置并且表示对象的其余内存被擦除为零(概念上,无论如何 – 它可能已经被擦除为零之前,作为垃圾收集的一部分)

  • 这种行为至少会导致一致性,但这可能是一种痛苦。 就一致性而言,假设您有一个可变基类的只读子类。 基类可能有一个isMutable()属性,该属性实际默认为true – 但是子类将其isMutable()为始终返回false。 在子类构造函数运行之前,对象是可变的是奇怪的,但之后是不可变的。 另一方面,在类的构造函数运行之前最终在类中运行代码的情况下,它确实很奇怪:(

一些指导原则:

  • 尽量不要在构造函数中做太多工作。 避免这种情况的一种方法是在静态方法中工作,然后使静态方法的最后一部分成为构造函数调用,它只是设置字段。 当然,这意味着你在做这项工作时不会得到多态性的好处 – 但是在构造函数调用中这样做无论如何都是危险的。

  • 在构造函数期间尽量避免调用非final方法 – 这很可能会引起混淆。 记录你必须 非常清楚的任何方法调用,以便任何覆盖它们的人知道它们将在初始化完成之前被调用。

  • 如果你必须在施工期间调用一个方法,那么通常不适合在之后调用它。 如果是这种情况,请记录并尝试在名称中指明它。

  • 尽量不要过度使用inheritance – 当你从一个非Object类以外的超类派生的子类时,这只会成为一个问题。inheritance的设计很棘手。

如果尚未创建Derived类对象,为什么会调用重写的方法?

Derived类构造函数隐式调用Base类构造函数作为第一个语句。 Base类构造函数调用method() ,它调用Derived类中的重写实现,因为这是正在创建其对象的类。 Derived类中的method()在该点看到var为0。

在什么时间点,var赋值为0?

在调用Derived类的构造函数之前,为var赋予int类型的默认值,即0。 在隐式超类构造函数调用完成之后 ,在Derived类的构造函数中的语句开始执行之前 ,它被赋值为2。

是否存在需要此类行为的用例?

在非final类的构造函数/初始值设定项中使用非finalprivate方法通常是个坏主意。 原因在您的代码中很明显。 如果正在创建的对象是子类实例,则这些方法可能会产生意外结果。

请注意,这与C ++不同,C ++在构造对象时类型确实发生了变化,因此从基类构造函数调用虚方法不会调用派生类的覆盖。 同样的事情在破坏期间反过来发生。 因此,对于来到Java的C ++程序员来说,这可能是一个小陷阱。

为了解释这种行为,应该注意Java语言规范的一些属性:

  • 在子类的构造函数之前总是隐式/显式地调用超类的构造函数。
  • 来自构造函数的方法调用就像任何其他方法调用一样; 如果方法是非final,则调用是虚拟调用,这意味着要调用的方法实现是与对象的运行时类型相关联的实现。
  • 在构造函数执行之前,所有数据成员都使用默认值自动初始化(0表示数字基元,null表示对象,false表示布尔值)。

事件顺序如下:

  1. 创建子类的实例
  2. 使用默认值初始化所有数据成员
  3. 被调用的构造函数会立即将控制权委托给相关的超类构造函数。
  4. 超级构造函数初始化其部分/全部数据成员,然后调用虚方法。
  5. 该方法由子类覆盖,因此调用子类实现。
  6. 该方法尝试使用子类的数据成员,假设它们已经初始化,但事实并非如此 – 调用堆栈尚未返回到子类的构造函数。

简而言之,只要超类的构造函数调用非final方法,我们就有进入此陷阱的潜在风险,因此不建议这样做。 请注意,如果您坚持使用此模式,则没有优雅的解决方案。 这里有两个复杂且富有创意的,都需要线程同步(!):

http://www.javaspecialists.eu/archive/Issue086.html

http://www.javaspecialists.eu/archive/Issue086b.html