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
eis evaluated. If evaluation ofecompletes 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.fis not static and the result of the evaluation ofeabove isnull, then aNullPointerExceptionis thrown.[...]
Indeed, foo will be evaluated first, then setUpFoo, and only then the null check happens, on the old value of foo.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With