javanullpointerexceptioninitialization-order

NPE after assigning object - Initialization order


Setup

I recently stumbled upon an interesting situation on the topic of execution order and behavior on variable assignments. Have a look at the following snippet:

static class Foo {
  int x = 0;
  Foo() {}
}
    
static Foo foo = null;
    
public static void main(String[] args) {
  System.out.println("Before foo: " + foo);
  foo.x = setupFoo();
  System.out.println("After foo.x: " + foo.x);
}

static int setupFoo() {
  foo = new Foo();
  foo.x = 1;
  System.out.println("Setup foo.x: " + foo.x);
  return 2;
}

The output is:

Before foo: null
Setup foo.x: 1
Exception in thread "main" java.lang.NullPointerException:
  Cannot assign field "x" because "Test.foo" is null

(Executed on OpenJDK Temurin-21.0.1+12)

Observation

Now, I would have expected this to either throw immediately at foo.x = setupFoo() already, because foo at that moment is still null, or alternatively that it does not throw at all because setupFoo() does properly setup the instance foo = new Foo().

But for some reason it does a mix. It enters the method despite foo being null when foo.x = setupFoo(), then fully goes through setupFoo() that would setup foo properly. But then when coming back to main it crashes, saying that foo is null - which it was originally but is not anymore.

Question

I would like to understand why it is behaving as observed. Which parts in the JLS come at play here?

Perhaps someone also knows the reasoning behind this.

Details

After removing the prints, this is the relevant bytecode (javap -v):

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: (0x0009) ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=1, args_size=1
      0: getstatic     #7          // Field foo:LTest$Foo;
      3: invokestatic  #13         // Method setupFoo:()I
      6: putfield      #17         // Field Test$Foo.x:I
      9: return
    LineNumberTable:
    line 10: 0
    line 11: 9

static int setupFoo();
  descriptor: ()I
  flags: (0x0008) ACC_STATIC
  Code:
    stack=2, locals=0, args_size=0
      0: new           #18         // class Test$Foo
      3: dup
      4: invokespecial #23         // Method Test$Foo."<init>":()V
      7: putstatic     #7          // Field foo:LTest$Foo;
    10: getstatic     #7           // Field foo:LTest$Foo;
    13: iconst_1
    14: putfield      #17          // Field Test$Foo.x:I
    17: iconst_2
    18: ireturn
    LineNumberTable:
    line 14: 0
    line 15: 10
    line 16: 17

If I get it right, it appears that the behavior is mostly explained by this bit here:

0: getstatic     #7   // Field foo:LTest$Foo;
3: invokestatic  #13  // Method setupFoo:()I
6: putfield      #17  // Field Test$Foo.x:I

which:

  1. gets a reference to the field foo to assign to (currently null)
  2. then calls setupFoo() and processes it fully, making foo non-null
  3. and then trying to do the assignment foo.x but the reference foo was put on the stack before the method call, so it is still null in this context

Solution

  • This behaviour is as specified in JLS 15.26.1.

    If the left-hand operand expression is a field access expression e.f, possibly enclosed in one or more pairs of parentheses, then:

    • First, the expression e is evaluated. If evaluation of e completes abruptly, the assignment expression completes abruptly for the same reason.

    • Next, the right hand operand is evaluated. If evaluation of the right hand expression completes abruptly, the assignment expression completes abruptly for the same reason.

    • Then, if the field denoted by e.f is not static and the result of the evaluation of e above is null, then a NullPointerException is thrown.

    • [...]

    Indeed, foo will be evaluated first, then setUpFoo, and only then the null check happens, on the old value of foo.