【问题标题】:Problem in instance variable initialization实例变量初始化问题
【发布时间】:2011-03-18 08:17:53
【问题描述】:

这里有一些示例代码,

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());

  }
}

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

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

【问题讨论】:

  • value() 仅在 Base 中定义,并返回 val 仅在 Base 中定义并在 Base 构造函数中由“5”初始化。 lookup 是从 Base 构造函数中调用的,这意味着重载不起作用,因为 Derived 类还没有完全构造好 你对代码还有什么期望?
  • @ULysses,发布的代码在哪里重载?我认为你的意思是覆盖。
  • @ULysses,为什么不覆盖工作?

标签: java instance instance-variables


【解决方案1】:

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

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

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

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

通常,正是出于这个原因,从构造函数调用非最终方法是一个坏主意,许多静态分析工具会警告您不要这样做。这类似于在构造过程中让对象引用泄漏,您最终可能会得到一个使类级不变量无效的实例(在您的情况下,Derived 的 num“始终”为 10,但在某些时候它可以被视为 0)。

编辑:请注意,对于这种特殊情况,无需任何额外代码,您可以通过将num 设为常量来解决问题:

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

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

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

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

我将其包含在此处是为了比较和感兴趣,而不是因为它是一般意义上的这个“问题”的解决方法(因为它不是)。

【讨论】:

  • 那个lookup() 在那里只是假的,可以把它注释掉。
  • 我关于Java构造函数和模板方法模式的相关文章:novyden.blogspot.com/2011/08/…
【解决方案2】:

关于为什么在构造基类时不能访问子类字段已经有很多很好的答案,但我想你问的是如何:像这样的工作解决方案:

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()...

【讨论】:

  • 从子类构造函数调用init()的一个问题是这样的方法不能写入任何final字段。如果每个级别的构造函数采用object,它要么将其传递给超类构造函数,要么封装在一个对象中,该对象提供给超类构造函数,并且每个级别都有一个虚拟方法getConstructorParam(),它返回传递的object到它的构造函数(如果需要,从super.getConstructorParam()返回的那个中提取),那么子类应该能够在基本构造函数完成之前完成它们的部分初始化。
  • 我会质疑任何最终字段打算由子类初始化的设计;)
【解决方案3】:

让我们慢慢来:

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 步,调用 d.value(),它属于 Base,而 val 由于 1 而未设置,因此得到的是 0 而不是 10。

【讨论】:

    【解决方案4】:

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

    • 调用派生构造函数
      • 基础构造函数作为其第一个操作被调用
      • 基础构造函数调用查找
    • 派生构造函数继续并将 num 初始化为 10

    由于基构造函数调用lookup时子类构造函数没有完成,所以对象还没有完全初始化,lookup返回num字段的默认值。

    【讨论】:

    • +1,吸取的教训:从构造函数调用非最终方法确实是不好的做法。
    【解决方案5】:

    当构造函数调用 this 时,下面的代码将返回 0(通过查看程序,您会期望 10)。原因很简单,num还没有初始化,父类调用了这个方法。

    public int lookup() {
        return num;
    }
    

    【讨论】:

      【解决方案6】:

      您在Derived 类中重写了方法lookup(),因此当调用Base 构造函数时,它会调用来自Derived 的方法,该方法的主体为return num。在 Base 初始化时,Derivednum 实例变量尚未初始化,为 0。这就是为什么在 Base 中将 val 分配为 0。

      如果我正确理解您的意图,您应该将Base 中的value 方法更改为:

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

      【讨论】:

        【解决方案7】:

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

        一般来说,在派生实例字段初始化之前调用基构造函数。

        【讨论】:

        • 这意味着派生类的方法可以运行,即使它的实例变量还没有被初始化?
        • @Zaki,是的,没错。这就是为什么,正如 Andrzej Doyle 所提到的,从构造函数调用非最终方法是一个坏主意。我同意这种行为是违反直觉的,即使它具有一定的一致性(基类在派生类之前初始化)。
        猜你喜欢
        • 2017-04-05
        • 2010-11-26
        • 1970-01-01
        • 1970-01-01
        • 2014-09-14
        • 2013-07-04
        • 2010-10-31
        • 1970-01-01
        • 2012-09-17
        相关资源
        最近更新 更多