当嵌套枚举在其构造函数中引用父静态成员时,为什么会出现 NPE?
Why do I get an NPE when a nested Enum references a parent static member in its constructor?
重建条件(据我所知):
- 嵌套枚举引用父静态成员
- 嵌套 class
- 父class的静态成员将枚举作为嵌套class
的构造函数参数
- enum 在父 class
中的任何其他内容之前被外部 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());
}
}
这是正在发生的事情:
- main方法开始执行
- 你指的是
Recreate.Car.TESLA
- class加载器开始加载和初始化
enum Car
。如下所述,class Recreate
尚未加载或初始化。
TESLA
的初始值设定项是指 FEATURES
- 这会导致 class
Recreate
被加载和初始化
- 作为
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
...
- 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
因此,出于我们的目的,这里使用了三个静态非常量变量:FEATURES
、TESLA
和 ONE_CAR_GARAGE
。
现在,在您的工作案例中,您引用 Recreate.ONE_CAR_GARAGE
。这是对 Recreate
中静态字段的引用,因此 FEATURES
和 ONE_CAR_GARAGE
得到初始化。然后,在 ONE_CAR_GARAGE
的初始化期间,TESLA
被初始化,因为它的枚举 class 被引用。一切顺利。
但是,如果我们过早地引用枚举,那么我们就会以错误的顺序进行操作。 Recreate.Car.TESLA
被引用,所以 TESLA
被初始化。 TESLA
引用 FEATURES
,所以 Recreate
必须被初始化。这导致 FEATURES
和 ONE_CAR_GARAGE
在 之前 TESLA
完成现有的初始化。
正是这种隐藏的依赖性让你绊倒了。 Recreate.Car
取决于 Recreate
,而 Recreate
又取决于 Recreate.Car
。将 ONE_CAR_GARAGE
字段移动到 Garage
class 将导致它无法使用 FEATURES
进行初始化,这将解决您的问题。
重建条件(据我所知):
- 嵌套枚举引用父静态成员
- 嵌套 class
- 父class的静态成员将枚举作为嵌套class 的构造函数参数
- enum 在父 class 中的任何其他内容之前被外部 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());
}
}
这是正在发生的事情:
- main方法开始执行
- 你指的是
Recreate.Car.TESLA
- class加载器开始加载和初始化
enum Car
。如下所述,classRecreate
尚未加载或初始化。 TESLA
的初始值设定项是指FEATURES
- 这会导致 class
Recreate
被加载和初始化 - 作为
Recreate
静态初始化的一部分,加载、初始化 ClassGarage
,并创建实例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
...
- 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
因此,出于我们的目的,这里使用了三个静态非常量变量:FEATURES
、TESLA
和 ONE_CAR_GARAGE
。
现在,在您的工作案例中,您引用 Recreate.ONE_CAR_GARAGE
。这是对 Recreate
中静态字段的引用,因此 FEATURES
和 ONE_CAR_GARAGE
得到初始化。然后,在 ONE_CAR_GARAGE
的初始化期间,TESLA
被初始化,因为它的枚举 class 被引用。一切顺利。
但是,如果我们过早地引用枚举,那么我们就会以错误的顺序进行操作。 Recreate.Car.TESLA
被引用,所以 TESLA
被初始化。 TESLA
引用 FEATURES
,所以 Recreate
必须被初始化。这导致 FEATURES
和 ONE_CAR_GARAGE
在 之前 TESLA
完成现有的初始化。
正是这种隐藏的依赖性让你绊倒了。 Recreate.Car
取决于 Recreate
,而 Recreate
又取决于 Recreate.Car
。将 ONE_CAR_GARAGE
字段移动到 Garage
class 将导致它无法使用 FEATURES
进行初始化,这将解决您的问题。