NullPointerException | `this` inside enum construc

2019-02-04 09:02发布

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;
        }
    }
}

Can anyone explain me why there is NullPointerException in Platform1 but not in Platform2. How in the second case we are able to access the enum object and its properties, even before the object is constructed?

6条回答
我命由我不由天
2楼-- · 2019-02-04 09:33

You can't do that as you trying to work on the enum before it has been properly constucted. (As in the full thing constructed). You will notice that the error is trying to refer to the values part of the enum:

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

You need to allow the object to be properly internally intialised before working on it. This will work:

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);
    };

Obviously your initialise function should be renamed as it's just reporting the value. Your second example provides values and so works correctly.

From one of the Java docs:

The enum declaration defines a class (called an enum type). The enum class body can include methods and other fields. The compiler automatically adds some special methods when it creates an enum. For example, they have a static values method that returns an array containing all of the values of the enum in the order they are declared. This method is commonly used in combination with the for-each construct to iterate over the values of an enum type. For example, this code from the Planet class example below iterates over all the planets in the solar system.

查看更多
Bombasti
3楼-- · 2019-02-04 09:34

You are getting a NPE because you are referencing an instance which hasn't been constructed yet. Platform1.FACEBOOK is null until Platform1 constructor which constructs FACEBOOK instance is completed.

The Platform1 constructor calls initialize, which contains a switch. The case in that switch reads Platform1.FACEBOOK. Since FACEBOOK's constructor didn't return yet then the FACEBOOK reference is null. Java Language Specification doesn't allow null as a case in switch, it will throw a runtime exception just as you found.

查看更多
Fickle 薄情
4楼-- · 2019-02-04 09:45

As it has already been pointed out the switch on enums internally calls the values method, but that would only be initialized after all enum constants were initialized:

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

In the Platform2, this doesn't happen because the switch in on strings.

A more object-oriented approach would be to create a initialize method that the constructor calls and is overridden by the constants that need a specialized initialization:

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());
    }
}
查看更多
成全新的幸福
5楼-- · 2019-02-04 09:45

The example below shows the initializing lifecycle:

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
    }
}

In a nutshell, the enum constant was not initialized during building itself. In other words, the current enum instance will be assigned to the associated constant until the whole building work completed.

The switch statement will calling values() method, however the enum constants are in building and didn't ready for using. In order to avoid the client code to change its internal $VALUES array, the values() will clone its internal array, due to the enum constants is not ready yet then the NullPointerException was thrown. here is the bytecode values() method & static initializing block:

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;
查看更多
ゆ 、 Hurt°
6楼-- · 2019-02-04 09:50

Short Answer: the place where your calling initialize method, is getting called when that enum class is getting loaded by the class loader(in progress), and hence you can not access class level properties i.e static. Where as you can access the non-staic properties.

1. The constructor of enum gets called when you first time refer that Enum in code.

Platform1 p1=Platform1.FACEBOOK;

This line will Load the class for Enum Platform1 using the class loader. And the constructor will get called for each entry/instance in that enum, here it is 3.

Below code will print three hash codes.

   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;
      }
    }
  }

So in short when this initialize method is getting called the Enum class is not fully loaded and it is in progress. And hence you cannot access any static property or method of that enum at that point of time.

2. When you use below line, it calls values() a static method,

 public void initialize(Platform1 platform){
      switch (platform) {
      }
    }

Just change the static method to some equivalent non-static method and things will work. like,

  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;
      }
    }
  }

3. So the answer to your question is, when Enum class is getting initialized, you

  • can not access any static things

  • but can access non static things

hence this below code is working gor you,

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. Here if you change the displayName to static, things will break.

  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;
      }
    }
  }
查看更多
趁早两清
7楼-- · 2019-02-04 09:52

Exactly. Just as @PeterS mentioned using enum before it has been properly constructed is causing NPE, because values() method is being called on un-constructed enum.

One more point, I would like to add here that Platform1 and Platform2 both are trying to use unconstructed enum in switch() but NPE is only in Platform1. Reason behind this is as follows :-

 public void initialize(Platform1 platform){
        switch (platform) {

Above piece of code from Platform1 enum is using platform enum object in switch where internally $SwitchMap$Platform1[] array is used and to initialize this array values() method is utilized, thus you get NPE. But in Platform2, switch (platform.displayName) is comparison on displayName which is already initialized and a string comparison occurs thus no NPE.

Following are fragments of decompiled code :-

Platform1

 static final int $SwitchMap$Platform1[] =
            new int[Platform1.values().length];

Platform2

switch ((str = platform.displayName).hashCode())
    {
    case 3260: 
      if (str.equals("fb")) {
查看更多
登录 后发表回答