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)
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.
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.
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:
foo
to assign to (currently null
)setupFoo()
and processes it fully, making foo
non-nullfoo.x
but the reference foo
was put on the stack before the method call, so it is still null
in this contextThis 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 ofe
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 ofe
above isnull
, then aNullPointerException
is thrown.[...]
Indeed, foo
will be evaluated first, then setUpFoo
, and only then the null check happens, on the old value of foo
.