可以在完全创建对象之前调用对象方法吗?

Can a objects method be called before a object is fully created?

我收到了一位用户的崩溃报告,似乎不太可能。堆栈跟踪表明一个对象为 null,他得到了一个 nullpointerException。

(想看的话这里是台词)

public class City extends Unit {
    private ArrayList<SolderType> Queue = new ArrayList<SolderType>();

    public float getPrecentCompleted()
    {
        if(Queue.isEmpty())
        {
            return 0f;
        }
        //More code that is not relevent
    }
}

解释 Queue 可能为 null 并没有遗漏太多内容,但是 Queue 仅在我的代码中的一个位置创建,即在构造函数中。所以我不明白它怎么可能为空。该对象在多个线程上共享,并且一直在创建新对象。但是Queue点只能在创建对象的时候设置。所以我不明白这怎么可能。是否有可能一个线程调用对象方法,而另一个线程创建对象但未完成?

编辑 添加了更多可能与问题相关的代码。

造成这种情况的最常见原因是从 superclass 构造函数调用重写方法,如下所示:

import java.util.ArrayList;


public class NPEInheritance {
    static class Parent {
        Parent() {
            validate();
        }

        void validate() {}
    }

    static class Child extends Parent {
        private ArrayList<Object> Queue;

        Child() {
            Queue = new ArrayList<>();
        }

        @Override
        void validate() {
            if(Queue.isEmpty()) {
                System.out.println("Queue is empty");
            }
        }
    }

    public static void main(String[] args) {
        new Child();
    }
}

当你运行这段代码时,你会看到这样的"impossible NullPointerException"。如您所见,此处 Parent 构造函数调用了 Child class 中重写的方法,重写方法使用尚未初始化的字段,因为 Child 构造函数仍未执行.

我认为,当您创建子对象时,它首先会初始化父对象(因为它会扩展),并且当您的父对象尝试验证其时间时,您将获得 NullPointer。 我认为您需要像这样将 Queue 对象放入父对象中。

import java.util.ArrayList;

public class NPEInheritance {
    static class Parent {

        protected ArrayList<Object> Queue;

        Parent() {
            Queue = new ArrayList<>();
            validate();
        }

        void validate() {}
    }

    static class Child extends Parent {

        Child() {
        }

        @Override
        void validate() {
            if(Queue.isEmpty()) {
                System.out.println("Queue is empty");
            }
        }
    }

    public static void main(String[] args) {
        new Child();
    }
}

当 class A 的静态初始化创建或引用 class B 的一个(可能是静态的)实例,并且 B 的静态初始化对 A 具有类似的依赖性时,会出现一个需要注意的特殊情况。

显然,在这种情况下,class 都不能先于另一个完全初始化。

Java 通过允许 class 之一成为 return 未初始化的值来静静地打破僵局。因此,在没有警告的情况下,您可能会看到 "impossible" 个零或空值。由于此依赖循环可能会经历其他几个 classes,即使您知道存在此风险,追踪起来也确实很痛苦。

修复通常是将涉及的部分或全部静态对象重构为第三个 class,使依赖图再次成为树。一旦发现问题,就简单明了。当然,最好从一开始就避免制造问题。

评论给出了正确答案:是的,这是可能的,除非您添加了同步来防止它。定义两个线程之间顺序的唯一操作如下(参见 https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.4):

  1. 监视器 m 上的解锁操作与所有后续锁定同步 对 m 的操作(其中 "subsequent" 是根据 同步顺序)。
  2. 写入易失性变量 v (§8.3.1.4) 与所有同步 任何线程对 v 的后续读取(其中定义了 "subsequent" 根据同步顺序)。
  3. 启动线程的操作与中的第一个操作同步 它启动的线程。
  4. 将默认值(零、假或空)写入每个 变量与每个线程中的第一个动作同步。尽管在分配包含变量的对象之前将默认值写入变量似乎有点奇怪,但从概念上讲,每个对象都是在程序开始时使用其默认初始化值创建的。
  5. 线程 T1 中的最终操作与线程中的任何操作同步 另一个检测到 T1 已终止的线程 T2。
  6. 如果线程T1中断了线程T2,由T1中断 与任何其他线程(包括 T2)所在的任何点同步 确定 T2 已被中断(通过有一个 InterruptedException 抛出或通过调用 Thread.interrupted 或 Thread.isInterrupted).

如果没有这样的操作,您将看到其他线程中的对象处于未定义状态。

解决空指针异常的一种方法是使用 final 字段(参见 https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5)。但是你还必须同步 ArrayList。我建议使用 java.util.concurrent 包中的队列。