4

重新创建的条件(据我所知):

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

在线运行此代码: 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());
  }
}
4

2 回答 2

4

这是正在发生的事情:

  1. main方法开始执行
  2. 你指的是Recreate.Car.TESLA
  3. 类加载器开始加载和初始化enum Car. 如下所述,类Recreate尚未加载或初始化。
  4. 的初始化器TESLA是指FEATURES
  5. 这会导致类Recreate被加载和初始化
  6. 作为 的静态初始化的一部分,加载、初始化RecreateClass并创建实例。GarageONE_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。这是由您在类之间创建的复杂依赖关系决定的,因此解决方案是解开依赖关系。你如何做到这一点将取决于你的最终目标。

于 2018-07-13T04:54:48.337 回答
2

你有一个隐藏的循环依赖,这让 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. 详细的初始化过程

...

  1. 接下来,按照文本顺序执行类的类变量初始化程序和静态初始化程序,或者接口的字段初始化程序,就好像它们是一个单独的块一样。

所以我们的静态数据在第一次被引用时就被初始化了。现在,你Car.TESLA是隐含static final的,但它不是一个常数,根据定义

常量变量是原始类型或 String 类型的最终变量,使用常量表达式初始化

因此,出于我们的目的,这里有三个静态非常量变量:FEATURESTESLAONE_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并解决您的问题。

于 2018-07-13T04:58:54.463 回答