【问题标题】:Why are member objects initialized after the super class's constructor?为什么成员对象在超类的构造函数之后初始化?
【发布时间】:2012-02-09 06:11:55
【问题描述】:

我昨天遇到了一个有趣的问题,虽然修复很简单,但我仍然对它的“原因”有点模糊。

我有一个类,它有一个在实例化时分配的私有成员变量,但是如果它用于由超类的构造函数调用的抽象函数中,则该变量没有值。问题的解决方案非常简单,我只需将变量声明为 static 并正确分配它。一些代码来说明问题:

class Foo extends BaseClass
{
    private final String bar = "fooBar!";
    public Foo()
    {
        super();
    }

    @Override 
    public void initialize()
    {
        System.out.println(bar);
    }
}

还有基类:

abstract class BaseClass
{
    public BaseClass()
    {
        initialize();
    }

    public abstract void initialize();
}

在这个例子中,当我们调用 new Foo(); 时,它会输出 (null) 而不是预期的 fooBar!

既然我们实例化了一个 Foo 类型的对象,在调用它的(以及它的超类的)构造函数之前不应该分配和分配它的成员吗?这是在 Java 语言中的某个地方指定的还是特定于 JVM 的?

感谢您的任何见解!

【问题讨论】:

  • 警惕从超类ctor调用子类方法。
  • 代码实际上打印了fooBar!因为bar 变量是最终的,这使它成为编译时常量。如果没有 final,它将打印 null。
  • @x22 这实际上是不正确的,是什么导致了调查这种情况 - 我最初写它时也相信同样的事情。

标签: java


【解决方案1】:

我只想在已接受的答案中添加一件事,因为我不完全同意他的结论。

我们都做到了。

class Engine {
  public Engine() {
    init();
  }

  void init() {
    lockDoors();
    releasePressure();
    tightenSeatbelts();
    launchRocket();
  }

  ...
}

现在的问题是,我们应该向init() 函数添加哪个访问修饰符。它应该是私有的还是受保护的。

  • 将其设为私有
  • 使其受保护

在您做出选择之前

现在首先,您应该意识到(几乎)Engine 类中的所有代码都可以替换为子类。

  • 公共函数中的代码,很容易被覆盖
  • 受保护函数中的代码,很容易被覆盖
  • 私有函数中的代码,可以通过覆盖所有调用它的方法来替换。

嗯,只有一个例外:

  • 你永远不能修改构造函数的代码
  • 您永远无法避免从超类的构造函数中调用私有方法。
  • (当然,您不能替换 final 方法)

受保护的 init() - 错误的方式

假设init() 方法是protected 确实有一个陷阱。很容易覆盖它以添加如下功能。那确实是个错误。

class SubEngine extends Engine {
  int screws = 5;

  void init() {
    tightenScrews();
    super.init();
  }

  void tightenScrews() {
    // this won't print 5, but it will print 0.
    System.out.println("tightening " + screws + " screws"); 
  }
}

受保护的 init() - 正确的方式

因此,基本上,您应该禁用父代码并将执行推迟到您自己的构造函数。

class SubEngine extends Engine {
  int screws = 5;

  public SubEngine() {
    initSubEngine();
  }

  void init() {
    // disable parent code
  }

  void initSubEngine() {
    tightenScrews();
    super.init();
  }

  void tightenScrews() {
    // this will print 5 as expected
    System.out.println("tightening " + screws + " screws"); 
  }
}

私人 init() - 你可能需要一个电话

现在,如果 init() 方法是 private 呢?

  • 如上所述,无法禁用父构造函数的代码。如果 init() 是私有的,您根本无法禁用它。
  • 您最终会复制整个 Engine 类,可能只是添加 1 行代码。
  • 这可能还没有结束。即使在复制了您的类之后,您复制的对象也不会是 Engine,这意味着您将无法使用您的 EngineUtil#inspectEngine(Engine engine) 函数。
  • 也许有人事先知道这一点,并制作了一个IEngine 接口。然后你就可以摆脱它了。
  • 实际上,这意味着您必须拿起手机,致电开设Engine 课程的其他部门,并要求他们稍微更改代码以消除一些限制。

智能设计

还有另一种方法。构造函数用于设置变量。他们不应该激活任何东西。每次您看到一个类从其构造函数(或通过私有方法)创建new Thread 时,这应该是一个危险信号。

class Engine {
  public Engine() {
  }

  public void init() {
    lockDoors();
    releasePressure();
    tightenSeatbelts();
    launchRocket();
  }

  // and you probably also want one of these
  public void shutdown() { ... }

  ...
}

意图

当然,您的意图很可能不是公开您的代码。也许您真的不想让其他人扩展您的课程。在某些情况下,您肯定会想把人们拒之门外。

请注意,这也会使为您的代码编写测试变得更加困难。

无论如何,这是一个不同的场景。

【讨论】:

    【解决方案2】:

    bar = "fooBar!"; 的赋值在编译期间内联到构造函数中。

    超类构造函数运行before子类构造函数,因此语句之后执行是很自然的。

    不过,一般来说,bad practice 从构造函数中调用可覆盖的方法。

    【讨论】:

      【解决方案3】:

      它由 Java 语言规范定义。将其更改为静态几乎永远不会成为现实世界中可接受的解决方案。

      JLS 4.12.5 Initial Values of VariablesJLS 8.3.2 Initialization of Fields

      总的来说,从构造函数调用非最终方法是不好的做法。原因是它可以(如果方法是抽象的,那么肯定会)调用尚未初始化的类中的方法:当new Foo() 被执行时,BaseClass 初始化器(构造函数)被调用 before Foo 构造函数,所以 Foo.initialize 本质上是在处理尚未完全构造的对象。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-12-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-11-10
        • 2012-05-25
        相关资源
        最近更新 更多