重新创建的条件(据我所知):
- 嵌套枚举引用父静态成员
- 嵌套类 父类的
- 静态成员将枚举作为嵌套类的构造函数参数
- 枚举先于父类中的其他任何内容被外部类引用
联机运行此代码: 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());
}
}
以下是正在发生的事情:
- 主方法开始执行
- 你指的是
Recreate.Car.TESLA
- 类装入器开始装入并初始化
enum Car
。如下所述,类Recreate
尚未加载或初始化。 TESLA
的初始值设定项是指FEATURES
- 这会导致类
Recreate
被加载和初始化 - 作为
Recreate
静态初始化的一部分,类Garage
被加载、初始化,并创建实例ONE_CAR_GARAGE
。
这里的问题是,此时enum Car
的建设还没有完成,Car.TESLA
具有价值null
。
即使类可能是嵌套的,嵌套类也不会作为外部类初始化的一部分进行加载和初始化。 它们可能看起来嵌套在源代码中,但每个类都是独立的。 静态嵌套类等效于顶级类。 非静态类也是相同的,但能够通过隐藏引用引用包含类中的成员。
您可以亲自查看是否在调试器中运行它,在多个位置放置断点,并检查每个断点处的堆栈。
我使用以下代码在 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
中返回。 这是由您在类之间创建的复杂依赖项决定的,因此解决方案是解开依赖项。 你如何做到这一点将取决于你的最终目标。
你有一个隐藏的循环依赖关系,它混淆了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);
}
我们还需要 JLS 页面中的一些片段。
类或接口类型 T 将在以下任一情况首次出现之前立即初始化:
- 使用 T 声明的静态字段,并且该字段不是常量变量 (§4.12.4)。
12.4.2. 详细的初始化过程
。
- 接下来,执行 类或接口的字段初始值设定项,按文本顺序排列,就好像它们一样 是一个单一的块。
因此,我们的静态数据在首次引用时就被初始化了。现在,您的Car.TESLA
是隐式static final
,但根据定义,它不是一个常量。
是基元类型或使用常量表达式初始化的字符串类型的最终变量
因此,为了我们的目的,这里有三个静态非常量变量在起作用:FEATURES
、TESLA
和ONE_CAR_GARAGE
。
现在,在您的工作案例中,您引用Recreate.ONE_CAR_GARAGE
.这是对Recreate
中静态字段的引用,因此FEATURES
然后ONE_CAR_GARAGE
初始化。然后,在初始化ONE_CAR_GARAGE
期间,TESLA
被初始化,因为它的枚举类被引用。一切都很好。
但是,如果我们过早地引用枚举,那么我们就会以错误的顺序做事。Recreate.Car.TESLA
被引用,因此TESLA
被初始化。TESLA
引用FEATURES
,所以Recreate
必须初始化。这会导致FEATURES
和ONE_CAR_GARAGE
在TESLA
完成现有之前进行初始化。
正是这种隐藏的依赖性让你陷入困境。Recreate.Car
取决于Recreate
取决于Recreate.Car
。将ONE_CAR_GARAGE
字段移动到Garage
类中将导致它无法使用FEATURES
进行初始化,并将解决您的问题。