当嵌套枚举在其构造函数中引用父静态成员时,为什么会出现 NPE?

Why do I get an NPE when a nested Enum references a parent static member in its constructor?

重建条件(据我所知):

  1. 嵌套枚举引用父静态成员
  2. 嵌套 class
  3. 父class的静态成员将枚举作为嵌套class
  4. 的构造函数参数
  5. enum 在父 class
  6. 中的任何其他内容之前被外部 class 引用

运行这段在线代码: https://repl.it/repls/PlushWorthlessNetworking

import java.util.ArrayList;

class Recreate {

  private static ArrayList FEATURES = new ArrayList();

  public enum Car {
    TESLA(FEATURES);
    Car(ArrayList l) { }
  }

  public static class Garage {
    final Car car;

    Garage(Car car) {
      this.car = car;
    }
  }

  public static Garage ONE_CAR_GARAGE = new Garage(Car.TESLA);
}

class Main {
  public static void main(String[] args) {
    // inclusion of this line causes the next line to NPE
    System.out.println(Recreate.Car.TESLA);

    System.out.println(Recreate.ONE_CAR_GARAGE.car.toString());
  }
}

这是正在发生的事情:

  1. main方法开始执行
  2. 你指的是Recreate.Car.TESLA
  3. class加载器开始加载和初始化enum Car。如下所述,class Recreate 尚未加载或初始化。
  4. TESLA 的初始值设定项是指 FEATURES
  5. 这会导致 class Recreate 被加载和初始化
  6. 作为 Recreate 静态初始化的一部分,加载、初始化 Class Garage,并创建实例 ONE_CAR_GARAGE

这里的问题是,此时enum Car的构造还没有完成,Car.TESLA的值为null

即使 classes 可以嵌套,嵌套的 classes 也不会作为外部 class 初始化的一部分加载和初始化。它们可能看起来嵌套在源代码中,但每个 class 都是独立的。静态嵌套 classes 等同于顶级 classes。非静态 classes 也是相同的,但能够通过隐藏引用引用包含 class 中的成员。

如果您 运行 在调试器中执行此操作,在多个位置放置断点,并检查每个断点处的堆栈,您可以自己看看。

我 tested/debugged 在 Eclipse 中使用以下代码,并在指示的位置设置了断点。它与您的代码略有不同,但行为不应不同:

public class Foo5
{
    static class Recreate {

        private static ArrayList FEATURES = new ArrayList();

        public  enum Car {
          TESLA(FEATURES);
          Car(ArrayList l) { 
              System.out.println("car"); // *** Breakpoint ***
          }
        }
        public static Garage ONE_CAR_GARAGE = new Garage(Car.TESLA);

        public static class Garage {
            final Car car;

            Garage(Car car) {
              this.car = car;  // *** Breakpoint ***
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Recreate.Car car = Recreate.Car.TESLA;
        System.out.println(Recreate.Car.TESLA);
        System.out.println(Recreate.ONE_CAR_GARAGE.car.toString());
    }   
}

您遇到的第一个断点将是 Garage(Car car) 构造函数中的断点。此时检查堆栈你会看到

Foo5$Recreate$Garage.<init>(Foo5$Recreate$Car) line: 23 
Foo5$Recreate.<clinit>() line: 17   
Foo5$Recreate$Car.<clinit>() line: 12   
Foo5.main(String[]) line: 29    

所以当 Garage 构造函数被调用时,它还没有从创建 Car 返回。这是由您在 class 之间创建的复杂依赖关系决定的,因此解决方案是解开依赖关系。你如何做到这一点将取决于你的最终目标。

您有一个隐藏的循环依赖关系,它混淆了 JVM。让我们看看您的代码。

class Recreate {

  private static ArrayList FEATURES = new ArrayList();

  public enum Car {
    TESLA(FEATURES);
    Car(ArrayList l) { }
  }

  public static class Garage {
    final Car car;

    Garage(Car car) {
      this.car = car;
    }
  }

  public static Garage ONE_CAR_GARAGE = new Garage(Car.TESLA);
}

我们还需要 page out of the JLS 中的一些片段。

A class or interface type T will be initialized immediately before the first occurrence of any one of the following:

  • A static field declared by T is used and the field is not a constant variable (§4.12.4).

12.4.2. Detailed Initialization Procedure

...

  1. Next, execute either the class variable initializers and static initializers of the class, or the field initializers of the interface, in textual order, as though they were a single block.

所以我们的静态数据在第一次被引用时被初始化。现在,根据 the definition.

,您的 Car.TESLA 隐含地 static final,但它不是常数

A constant variable is a final variable of primitive type or type String that is initialized with a constant expression

因此,出于我们的目的,这里使用了三个静态非常量变量:FEATURESTESLAONE_CAR_GARAGE

现在,在您的工作案例中,您引用 Recreate.ONE_CAR_GARAGE。这是对 Recreate 中静态字段的引用,因此 FEATURESONE_CAR_GARAGE 得到初始化。然后, ONE_CAR_GARAGE 的初始化期间,TESLA 被初始化,因为它的枚举 class 被引用。一切顺利。

但是,如果我们过早地引用枚举,那么我们就会以错误的顺序进行操作。 Recreate.Car.TESLA 被引用,所以 TESLA 被初始化。 TESLA 引用 FEATURES,所以 Recreate 必须被初始化。这导致 FEATURESONE_CAR_GARAGE 之前 TESLA 完成现有的初始化。

正是这种隐藏的依赖性让你绊倒了。 Recreate.Car 取决于 Recreate,而 Recreate 又取决于 Recreate.Car。将 ONE_CAR_GARAGE 字段移动到 Garage class 将导致它无法使用 FEATURES 进行初始化,这将解决您的问题。