实例变量初始化中的问题

下面是一些示例代码,

class Base { private int val; Base() { val = lookup(); } public int lookup() { //Perform some lookup // int num = someLookup(); return 5; } public int value() { return val; } } class Derived extends Base { private int num = 10; public int lookup() { return num; } } 

 class Test { public static void main(String args[]) { Derived d = new Derived(); System.out.println("d.value() returns " + d.value()); } } 

输出:d.value()返回0 //我期望10,因为lookup()被覆盖,但不是0! 有人可以澄清一下吗?

Derived实例变量的初始化在其查找方法执行时尚未发生。 如何在调用其方法时确保Derived的实例变量已初始化?

好吧,首先,由于缺少someLookup方法,该代码无法编译。

无论如何,除此之外,我认为您的问题是由于构造函数的分层运行方式,您的预期无效。

超类’构造函数总是在子类’之前运行,这包括子类’变量的初始化器(它们实际上作为构造函数的一部分运行)。 因此,当您创建Derived实例时,会发生以下情况:

  1. 首先调用Base构造函数。
  2. 调用lookup() ,它使用Derived的实现。
  3. 返回num这是此时的默认值,因为尚未运行Derived的构造函数和初始值设定项
  4. val设置为0。
  5. Derived初始值设定项和构造函数都在运行 – 从这一点开始调用lookup将返回10。

一般来说,出于这个原因从构造函数调用非final方法是个坏主意,许多静态分析工具会警告你不要这样做。 它类似于在构造期间让对象引用泄漏,你最终会得到一个使类级不变量无效的实例(在你的情况下,Derived的num是“always”10但在某些点它可以看作是0)。

编辑:请注意,对于这种特殊情况,没有任何其他代码,您可以通过使num为常量来解决问题:

 class Derived extends Base { private static final int num = 10; ... 

这实际上可以做你想要的,因为静态初始化程序是在加载类时运行的(必须在调用构造函数之前发生)。 然而,这确实假设它适用于:

a)类的所有实例共享相同的num变量; b) num永远不需要改变(如果这是真的那么(a)自动为真)。

在您给出的确切代码中显然是这种情况,但我希望您可能省略额外的function以简洁。

我在这里包含这个用于比较和兴趣,而不是因为它是一般意义上的这个“问题”的解决方法(因为它不是)。

返回0的原因是在Derived中将10分配给num之前,正在调用构造函数Base(并在Derived中调用lookup)。

通常,在初始化派生实例字段之前调用基础构造函数。

在构造函数中调用可以在子类中重写的方法通常是个坏主意。 在您的示例中,发生以下情况:

  • 派生构造函数被调用
    • 基础构造函数被称为第一个操作
    • 基础构造函数调用查找
  • 派生的构造函数继续,初始值为num

由于在基础构造函数调用lookup时子类构造函数未完成,因此该对象尚未完全初始化,并且lookup返回num字段的默认值。

我们慢慢来:

 class Test { public static void main(String args[]) { // 1 Derived d = new Derived(); // 2 System.out.println("d.value() returns " + d.value()); } } 

步骤1,在Derived上调用(默认)构造函数,在设置num = 10之前,它链接到Base的构造函数,该构造函数调用Derived的查找方法,但是num尚未设置,因此val保持未初始化。

步骤2,调用属于Base的d.value(),由于1而未设置val,因此得到0而不是10。

关于为什么在构造基类时无法访问子类字段,已经有很多很好的答案,但我认为你问了一个如何 :这样的工作解决方案:

 public abstract class Animal { public Animal() { System.println(whoAmI()); } public abstract String whoAmI(); } public Lion() extends Animal { private String iAmA = "Lion"; public Lion(){super();} public String whoAmI() {return iAmA;} } 

实用的方法是在基类上引入一个init()方法,从子类的构造函数中调用它,如:

 public abstract class Animal { private boolean isInitialized = false; public Animal() {} void init() { isInitialized = true; System.out.println(whoAmI()); } public abstract String whoAmI(); public void someBaseClassMethod() { if (!isInitialized) throw new RuntimeException("Baseclass has not been initialized"); // ... } } public Lion() extends Animal { private String iAmA = "Lion"; public Lion() { super(); init(); } public String whoAmI() {return iAmA;} } 

唯一的问题是,您不能强制子类在基类上调用init()方法,并且可能无法正确初始化基类。 但是有了标志和一些例外,我们可以在运行时提醒程序员他应该调用init()

你在Derived类中有覆盖方法lookup() ,所以当调用Base构造函数时,它会调用Derived中的方法,该方法是return num 。 在Base初始化时, Derivednum实例变量尚未初始化并且为0.这就是为什么在Base中将val赋值为0的原因。

如果我理解你的意图,你应该将Basevalue方法更改为:

 public int value() { return lookup(); } 

当构造函数调用此代码时,下面的代码正在返回0(通过查看程序,你会期望10)。 原因很简单,num尚未初始化,父类调用此方法。

 public int lookup() { return num; }