为什么javac会在final字段中插入Objects.requireNonNull(this)?

38

考虑以下类:

class Temp {
    private final int field = 5;

    int sum() {
        return 1 + this.field;
    }
}

然后我编译和反编译这个类:

> javac --version
javac 11.0.5

> javac Temp.java

> javap -v Temp.class
  ...
  int sum();
    descriptor: ()I
    flags: (0x0000)
    Code:
      stack=2, locals=1, args_size=1
         0: iconst_1
         1: aload_0
         2: invokestatic  #3   // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
         5: pop
         6: iconst_5
         7: iadd
         8: ireturn

简单来说,javacsum() 编译成:

int sum() {
    final int n = 1;
    Objects.requireNonNull(this); // <---
    return n + 5;
}

这里的Objects.requireNonNull(this)是做什么的?有什么意义吗?这与可达性有关吗?

Java 8编译器类似,它会插入this.getClass()代替Objects.requireNonNull(this)

int sum() {
    final int n = 1;
    this.getClass(); // <---
    return n + 5;
}

我也尝试使用Eclipse编译它。它没有插入requireNonNull

int sum() {
    return 1 + 5;
}

所以这是javac特定的行为。


8
似乎它没有意识到与其他表达式相比,这对于“this”是不必要的。 - Holger
1
好的发现(+1)。另外,使用javac 15-ea也可以观察到相同的行为。进一步观察,删除final可以正常工作。 - Naman
尝试添加注释,int sum();... 代码: 栈=2,本地变量=1,参数大小=1 0: iconst_1 1: aload_0 2: invokestatic #13 // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object; 5: pop 6: iconst_5 7: iadd 8: ireturn 相对于删除最终关键字解析为int sum();... 代码: 栈=2,本地变量=1,参数大小=1 0: iconst_1 1: aload_0 2: getfield #7 // Field field:I 5: iadd 6: ireturn` - Naman
1个回答

41

由于该字段不仅是final,而且是一个编译时常量,因此在读取时不会被访问,但读取将被替换为常量值本身,例如您的情况下的iconst_5指令。

但是,在解引用null时抛出NullPointerException的行为(使用getfield指令时隐含),必须保留¹。因此,当您将该方法更改为

int sumA() {
  Temp t = this;
  return 1 + t.field;
}

Eclipse也会插入显式的null检查。

所以,我们在这里看到的是javac未能识别,在这种特定情况下,当引用为this时,非空属性由JVM保证,因此不需要显式的null检查。

¹ 参见JLS §15.11.1. 使用主要字段访问

  • 如果字段不是static

    • 评估Primary表达式。如果Primary表达式的评估突然完成,则出于同样的原因,字段访问表达式会突然完成。
    • 如果Primary的值为null,则抛出NullPointerException异常。
    • 如果该字段是非空白的final,则结果是在对象中找到的类型T中命名成员字段的值。


2
好的观点。这样演示甚至更容易:Temp t = null; return 1 + t.fieldObjects.requireNonNull(t),否则它将返回6而没有NPE。 - ZhekaKozlov
1
@MTilsted:不可以,因为在这种情况下,该字段本身是一个原始的“int”,它不能为null。 - Ilmari Karonen
1
@MTilsted 使用反射更改 final 字段可能会导致未定义的行为。其中之一是潜在地不影响内联值。 - k5_
1
@MTilsted 这个检查不是关于字段值的,而是关于字段所有者引用,即 this。该引用无法通过反射更改,并且不可能为 null,因为不可能使用 null 接收器引用进入实例方法;调用指令已经失败了。 - Holger
1
@TomHawtin-tackline,从使用getClassrequireNonNull的转换已在此问答中得到解决。 - Holger
显示剩余2条评论

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