当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
类的构造函数/初始值设定项中使用非final
非private
方法通常是个坏主意。 原因在您的代码中很明显。 如果正在创建的对象是子类实例,则这些方法可能会产生意外结果。
请注意,这与C ++不同,C ++在构造对象时类型确实发生了变化,因此从基类构造函数调用虚方法不会调用派生类的覆盖。 同样的事情在破坏期间反过来发生。 因此,对于来到Java的C ++程序员来说,这可能是一个小陷阱。
为了解释这种行为,应该注意Java语言规范的一些属性:
- 在子类的构造函数之前总是隐式/显式地调用超类的构造函数。
- 来自构造函数的方法调用就像任何其他方法调用一样; 如果方法是非final,则调用是虚拟调用,这意味着要调用的方法实现是与对象的运行时类型相关联的实现。
- 在构造函数执行之前,所有数据成员都使用默认值自动初始化(0表示数字基元,null表示对象,false表示布尔值)。
事件顺序如下:
- 创建子类的实例
- 使用默认值初始化所有数据成员
- 被调用的构造函数会立即将控制权委托给相关的超类构造函数。
- 超级构造函数初始化其部分/全部数据成员,然后调用虚方法。
- 该方法由子类覆盖,因此调用子类实现。
- 该方法尝试使用子类的数据成员,假设它们已经初始化,但事实并非如此 – 调用堆栈尚未返回到子类的构造函数。
简而言之,只要超类的构造函数调用非final方法,我们就有进入此陷阱的潜在风险,因此不建议这样做。 请注意,如果您坚持使用此模式,则没有优雅的解决方案。 这里有两个复杂且富有创意的,都需要线程同步(!):