【问题标题】:Why is the value of the instance field coming null?为什么实例字段的值变为空?
【发布时间】:2015-12-22 14:32:18
【问题描述】:

我有这段简单的代码。

abstract class X {
    X() {
        read();
    }

    private void read() {
        Object obj = new Object();
        readValue(obj);
    }
    protected abstract void readValue(Object obj);
}

class Y extends X {

    Object obj = null;
    Y() {
        super();
    }

    @Override
    protected void readValue(Object obj) {
        this.obj = obj;
    }

    void printer() {
        System.out.println("Object = " + obj);
    }
}

class Runner {
    public static void main(String[] args) {
        Y y = new Y();
        y.printer();
    }
}

当我运行上面的代码时,对象被打印为空。 (我得到 "Object = null"
令人惊讶的是,当我删除空声明时,在 Y 类中

Object obj;

打印对象的实际值。
类似 ("Object = java.lang.Object@3cd1a2f1")
为什么会观察到这种行为? “这”指的是什么?如果我们只是声明它,任何对象都会被 null 初始化,那么为什么会出现这种异常行为呢?

【问题讨论】:

    标签: java inheritance constructor overriding abstract-class


    【解决方案1】:

    对象为null,因为超类构造函数在子类构造函数之前运行,因此Object obj = null;语句在调用超类构造函数之后执行是很自然的。

    Object obj = null的赋值;在编译期间内联到构造函数中。这只能在现有实例中访问, 当您在构造函数中时,实例还不存在(它仍在构建中)。

    你可以通过将object object声明为static来实现object value(Object = java.lang.Object@3cd1a2f1)。

    静态对象 obj = null;

    一般来说,从构造函数中实时调用被覆盖的方法是不好的做法。

    【讨论】:

      【解决方案2】:

      obj 字段为空的原因是由于Y 的构造函数调用中发生的步骤顺序:

      1. Y 的构造函数调用最终调用具体类readValue 的超级构造函数Y,因此为obj 字段分配一个非空值。
      2. 超级构造函数完成后,实例字段obj由于变量初始化器被初始化为null:

        Object obj = null;
        

      当您删除 null 初始化程序时,它会变成一个简单的字段声明,无需在步骤 2 中执行实例初始化。

      恰当的解决方案不是删除null 初始化器,而是重新设计整个类层次结构。例如,由于readValue 的目的似乎只是一个变量的设置器,那么您不需要让它覆盖父类中的抽象方法。只需将其设置为单独的方法,并在Y的构造函数完成后调用即可。

      【讨论】:

        【解决方案3】:

        这说明了从超类构造函数调用子类中继承方法的危险。主要的危险是子类中变量的初始化程序在超类构造函数完成之后运行

        这就是发生的事情。

        1. y 的对象已创建。
        2. 调用超类构造函数X(),它调用read()
        3. read 方法创建一个新的Object 并将其传递给readValue,它在Y 中实现。
        4. Y 中的readValue 方法将obj 设置为新对象。
        5. 超类构造函数X() 完成,初始化程序现在Y 中运行,将obj 设置为null
        6. printer 方法打印 "Object = null"

        如果您删除Y 中的obj 声明,则没有初始化程序可运行,obj 变量保留其值。

        JLS, Section 12.5,声明:

        [A]新对象中的所有实例变量,包括在超类中声明的变量,都被初始化为其默认值(第 4.12.5 节)。

        就在对新创建对象的引用作为结果返回之前,使用以下过程处理指示的构造函数以初始化新对象:

        1. 将构造函数的参数分配给此构造函数调用新创建的参数变量。

        2. 如果此构造函数以同一类中另一个构造函数的显式构造函数调用(第 8.8.7.1 节)开始(使用 this),则评估参数并使用这五个相同的步骤递归地处理该构造函数调用。如果该构造函数调用突然完成,则此过程出于相同原因而突然完成;否则,继续第 5 步。

        3. 此构造函数不是以显式构造函数调用同一类中的另一个构造函数开始的(使用 this)。如果此构造函数用于 Object 以外的类,则此构造函数将以显式或隐式调用超类构造函数开始(使用 super)。使用这五个相同的步骤递归地评估超类构造函数调用的参数和过程。如果该构造函数调用突然完成,则此过程出于相同的原因突然完成。否则,继续第 4 步。

        4. 为这个类执行实例初始化器和实例变量初始化器,将实例变量初始化器的值分配给相应的实例变量,按照它们在文本中出现的从左到右的顺序类的源代码。如果执行这些初始化程序中的任何一个导致异常,则不会处理更多初始化程序,并且此过程会突然完成相同的异常。否则,继续第 5 步。

        5. 执行此构造函数的其余部分。如果该执行突然完成,则此过程出于同样的原因突然完成。否则,此过程正常完成。

        (强调我的)

        与 C++ 不同,Java 编程语言在创建新类实例期间没有为方法分派指定更改的规则。如果调用的方法在正在初始化的对象的子类中被覆盖,那么即使在新对象完全初始化之前,也会使用这些覆盖方法。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-04-08
          • 2018-11-07
          • 2022-01-24
          • 2013-10-29
          • 2015-01-06
          • 2011-06-06
          • 1970-01-01
          • 2013-06-15
          相关资源
          最近更新 更多