空指针异常 | 枚举构造函数中的 `this` 导致 NPE

22
public class Test {
    public static void main(String[] args) {
        Platform1 p1=Platform1.FACEBOOK; //giving NullPointerException.
        Platform2 p2=Platform2.FACEBOOK; //NO NPE why?
    }
}

enum Platform1{
    FACEBOOK,YOUTUBE,INSTAGRAM;
    Platform1(){
        initialize(this);
    };
    public void initialize(Platform1 platform){
        switch (platform) {
        //platform is not constructed yet,so getting `NPE`.
        //ie. we doing something like -> switch (null) causing NPE.Fine!
        case FACEBOOK:
            System.out.println("THIS IS FACEBOOK");
            break;
        default:
            break;
        }
    }
}

enum Platform2{
    FACEBOOK("fb"),YOUTUBE("yt"),INSTAGRAM("ig");
    private String displayName;
    Platform2(String displayName){
        this.displayName=displayName;
        initialize(this);
    };  
    public void initialize(Platform2 platform){
        switch (platform.displayName) {
        //platform not constructed,even No `NPE` & able to access its properties.
        //switch (null.displayName) -> No Exception Why?
        case "fb":
            System.out.println("THIS IS FACEBOOK");
            break;
        default:
            break;
        }
    }
}

有人能解释一下为什么在Platform1中会出现NullPointerException但在Platform2中不会吗?第二种情况下,我们是如何在对象构造之前访问枚举对象及其属性的?


你尝试过调试吗?请查看:https://dev59.com/GnVC5IYBdhLWcg3wqzLV - Vladimir Vagaytsev
6个回答

12

没错。就像 @PeterS 所提到的,在枚举被正确构建之前使用它会导致 NPE,因为 values() 方法是在未构建的枚举上调用的。

还有一点,我想在这里补充的是,Platform1Platform2 都试图在 switch() 中使用未构建的枚举,但只有 Platform1 会出现 NPE。其原因如下:

 public void initialize(Platform1 platform){
        switch (platform) {
上面的代码片段来自Platform1枚举,它在switch中使用了platform枚举对象,内部使用了$SwitchMap$Platform1[]数组,并使用values()方法初始化该数组,因此导致了空指针异常。但是在Platform2中,switch (platform.displayName)比较的是已经被初始化的displayName字符串,因此进行字符串比较时不会出现空指针异常。
以下是反编译代码片段:
 static final int $SwitchMap$Platform1[] =
            new int[Platform1.values().length];

平台2

switch ((str = platform.displayName).hashCode())
    {
    case 3260: 
      if (str.equals("fb")) {

3

在枚举完全构建之前,您不能这样做,因为您试图在枚举上工作。 您会注意到错误尝试引用枚举的值部分:

Caused by: java.lang.NullPointerException
    at Platform1.values

在操作对象之前,需要确保对象已经被正确地内部初始化。以下代码可以实现这个目的:

public static void main(String[] args) {
    Platform1 p1=Platform1.FACEBOOK;
    p1.initialize(p1);
    //Platform1.YOUTUBE giving NullPointerException why?
    Platform2 p2=Platform2.FACEBOOK;
    //NO NPE
}

enum Platform1{
    FACEBOOK,YOUTUBE,INSTAGRAM;
    Platform1(){
        //initialize(this);
    };

显然,您的初始化函数应该更名,因为它只是报告值。您的第二个示例提供值,因此可以正确工作。
来自Java文档之一:
枚举声明定义了一个类(称为枚举类型)。枚举类主体可以包括方法和其他字段。编译器在创建枚举时会自动添加一些特殊方法。例如,它们有一个静态值方法,返回按声明顺序包含枚举所有值的数组。此方法通常与 for-each 结构结合使用,以迭代枚举类型的值。例如,下面的 Planet 类示例中的代码遍历太阳系中的所有行星。

3

正如已经指出的那样,枚举类型上的switch内部调用values方法,但是只有在所有枚举常量初始化之后才会初始化该方法:

Caused by: java.lang.NullPointerException
    at Platform1.values(Test.java:17)
    at Platform1$1.<clinit>(Test.java:25)
    ... 4 more

Platform2中,这种情况不会发生,因为开关在字符串上。更多面向对象的方法是创建一个initialize方法,构造函数调用并被需要特殊初始化的常量覆盖。
enum Platform3 {
    FACEBOOK {
        @Override
        protected void initialize() {
            System.out.println("THIS IS FACEBOOK");
        }
    },
    YOUTUBE,
    INSTAGRAM;

    Platform3() {
        initialize();
    }

    // this acts as the default branch in the switch
    protected void initialize() {
        System.out.println("THIS IS OTHER PLATFORM: " + this.name());
    }
}

1
老实说,在我看来,这是唯一清晰的答案。最初,我想添加一个说“嘿,值为NULL”-只有在所有其他枚举常量之后才会创建...但似乎你已经做到了。你的回答得到了我的支持。你可以通过提供发生在字节码级别的伪代码(以防OP不知道如何读取字节码)来使这个答案更好。 - Eugene

1
您之所以遇到NPE错误是因为引用了尚未构造的实例。在构造FACEBOOK实例的Platform1构造函数完成之前,Platform1.FACEBOOKnullPlatform1构造函数调用initialize,其中包含一个switch语句。该switch中的case读取Platform1.FACEBOOK。由于FACEBOOK的构造函数尚未返回,因此FACEBOOK引用为空。Java语言规范不允许在switch中使用null作为case,这将导致运行时异常,就像您发现的那样。

问题不在于为什么我们得到了“NPE”,而在于当属性为空时如何使用它,这是两种情况下需要考虑的。 - user4768611
在第二种情况下,您的“case”是一个字面上的字符串值“fb”(不是null),因此您不会得到NPE。 - Boris B.
尝试将第一种情况中的switch改为if,看看会发生什么。 - Boris B.
将switch改成if-else - 没有什么神奇的事情会发生,我知道在case中的值应该是编译时常量,但这不适用于if-else语句。 - user4768611
在第二种情况下,您的情况是字面上的NPE不是在评估情况时发生的,而是在传递给switch的值中,您说“fb”(不是null),因此package.FACEBOOK再次不为null。 - user4768611

1
下面的示例展示了初始化生命周期:
public class Test {
    //                                v--- assign to `PHASE` after creation
    static final Serializable PHASE = new Serializable() {{

        //                               v---it is in building and doesn't ready...
        System.out.println("building:" + PHASE); //NULL

        System.out.println("created:" + this);//NOT NULL
    }};


    public static void main(String[] args) {
        //                            v--- `PHASE` is ready for use
        System.out.println("ready:" + PHASE); //NOT NULL
    }
}

简而言之,枚举常量在构建过程中未被初始化。换句话说,当前的枚举实例将分配给相关常量,直到整个构建工作完成。
switch语句将调用values()方法,但是枚举常量正在构建中,还没有准备好使用。为了避免客户端代码更改其内部$VALUES数组,values()方法将克隆其内部数组,由于枚举常量尚未准备好,因此会抛出NullPointerException异常。以下是values()方法和静态初始化块的字节码:
static {};
    10: putstatic     #14           // Field FACEBOOK:LPlatform1;
    23: putstatic     #16           // Field YOUTUBE:LPlatform1;
    //  putstatic  //Other Fields

    61: putstatic     #1            // Field $VALUES:[LPlatform1;
    // `$VALUES` field is initialized at last ---^

public static Platform1[] values();

    // v--- return null
    0: getstatic     #1 // Field $VALUES:[LPlatform1;

    // v--- null.clone() throws NullPointerException
    3: invokevirtual #2 // Method "[LPlatform1;".clone:()Ljava/lang/Object;

1
短回答:当枚举类正在被类加载器加载时,调用初始化方法的位置也会被调用,因此您无法访问类级别的属性,即静态属性。但是您可以访问非静态属性。
1. 当您在代码中首次引用该枚举时,将调用其构造函数。
Platform1 p1=Platform1.FACEBOOK;

这行代码将使用类加载器加载Enum Platform1类。对于该枚举中的每个条目/实例,构造函数都会被调用,本例中为3个。

以下代码将打印三个哈希码。

   enum Platform1{
    FACEBOOK,YOUTUBE,INSTAGRAM;
    Platform1() {
      initialize(this);
    };
    public void initialize(Platform1 platform){
      System.out.println(platform.hashCode()); // it will print three hash codes
      switch (platform.hashCode()) {
        case 1:
          System.out.println(platform);
          break;
        default:
          break;
      }
    }
  }

当调用initialize方法时,Enum类尚未完全加载且正在进行中。因此,在该时间点上,您无法访问该枚举的任何静态属性或方法。
当您使用以下行时,它会调用values()一个静态方法,
 public void initialize(Platform1 platform){
      switch (platform) {
      }
    }

只需将静态方法更改为一些等效的非静态方法,事情就能正常运行。例如:
  enum Platform1{
    FACEBOOK,YOUTUBE,INSTAGRAM;
    Platform1() {
      initialize(this);
    };
    public void initialize(Platform1 platform){
      System.out.println(platform.hashCode());
      switch (platform.toString()) { // toString() is non static method
        case "FACEBOOK":
          System.out.println(platform);
          break;
        default:
          break;
      }
    }
  }

因此,您的问题的答案是,在初始化Enum类时,您

  • 不能访问任何静态内容

  • 但可以访问非静态内容

因此,下面的代码对您有效:

enum Platform2{
    FACEBOOK("fb"),YOUTUBE("yt"),INSTAGRAM("ig");
    private String displayName;
    Platform2(String displayName){
        this.displayName=displayName;
        initialize(this);
    };  
    public void initialize(Platform2 platform){
        switch (platform.displayName) {
        case "fb":
            System.out.println("THIS IS FACEBOOK");
            break;
        default:
            break;
        }
    }
}

4. 如果你将displayName更改为static,会导致问题。

  enum Platform2{
    FACEBOOK("fb"),YOUTUBE("yt"),INSTAGRAM("ig");
    private static String displayName = "FACEBOOK";
    Platform2(String displayName){
      initialize(this);
    };
    public void initialize(Platform2 platform){
      switch (platform.displayName) {
        case "FACEBOOK":
          System.out.println(platform);
          break;
        default:
          break;
      }
    }
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接