从字节码角度分析java三元运算符

145 阅读4分钟

需求开发的过程中,排查了一个很久的npe错误,最终发现原来是java三元运算符导致的问题。下面通过简单的几段代码去复现遇到的问题。

main方法如下:

public static void main(String[] args) {
  User user = new User();
  boolean flag = false;
  User.builder().age(flag ? getUserAge() : user.getAge()).build();
}

getUserAge方法如下:

private static int getUserAge() {
  return 5;
}

User类如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
  private Integer age;
  private String name;
}

main方法在执行时会报npe,分析了下可能导致npe的原因:

  1. User类中age字段数据类型是int
  2. user对象为null

很显然不是上面两个问题导致的。那现在就剩下这个三元运算符了,因为在排查其他问题的过程中,发现getUserAge这个方法的返回类型是基本数据类型int,而user.getAge()的返回类型是包装数据类型Integer,猜测可能是三元运算符或者是两个返回类型不同导致的npe。

单独执行一下三元运算符,发现返回的类型是默认是int类型:

// 类型默认返回int类型
int i = flag ? getUserAge() : user.getAge();

因为flag是false,所以始终执行的是user.getAge(),又因为user.getAge()的值为null,把null赋值给int类型的变量i,所以报了npe?那把变量i的类型改成Integer,应该就可以了

Integer i = flag ? getUserAge() : user.getAge();

发现代码依然会报npe,看来根本原因并不是把null赋值给int类型导致的npe,那就是根本就没到把null赋值给变量i这步,在某些地方就已经报npe。总共就这几行代码,还能是哪里报npe?

使用javap -c指令,将对应代码编译成JVM字节码,看看jvm在底层是如何执行这段代码的:

public static void main(String[] args) {
    User user = new User();
    boolean flag = false;
    int i = flag ? getUserAge() : user.getAge();
    User.builder().age(i).build();
}

上述代码对应的字节码如下:

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/example/demo/model/User
       3: dup
       4: invokespecial #3                  // Method com/example/demo/model/User."<init>":()V
       7: astore_1
       8: iconst_0
       9: istore_2
      10: iload_2
      11: ifeq          20
      14: invokestatic  #4                  // Method getUserAge:()I
      17: goto          27
      20: aload_1
      21: invokevirtual #5                  // Method com/example/demo/model/User.getAge:()Ljava/lang/Integer;
      24: invokevirtual #6                  // Method java/lang/Integer.intValue:()I
      27: istore_3
      28: invokestatic  #7                  // Method com/example/demo/model/User.builder:()Lcom/example/demo/model/User$UserBuilder;
      31: iload_3
      32: invokestatic  #8                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      35: invokevirtual #9                  // Method com/example/demo/model/User$UserBuilder.age:(Ljava/lang/Integer;)Lcom/example/demo/model/User$UserBuilder;
      38: invokevirtual #10                 // Method com/example/demo/model/User$UserBuilder.build:()Lcom/example/demo/model/User;
      41: pop
      42: return

重点关注下面这行:

24: invokevirtual #6 // Method java/lang/Integer.intValue:()I

在把结果赋值给变量i之前,先进行了一次拆箱,执行了Integer的intValue方法,然后再去赋值给变量i。也就是得到User.getAge()的返回值也就是null之后,执行null.intValue()时就报npe了。

那为什么手动调整变量i的数据类型为Integer后,还报npe呢,也就是如下代码:

public static void main(String[] args) {
    User user = new User();
    boolean flag = false;
    Integer i = flag ? getUserAge() : user.getAge();
    User.builder().age(i).build();
}

其对应的字节码如下:

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/example/demo/model/User
       3: dup
       4: invokespecial #3                  // Method com/example/demo/model/User."<init>":()V
       7: astore_1
       8: iconst_0
       9: istore_2
      10: iload_2
      11: ifeq          20
      14: invokestatic  #4                  // Method getUserAge:()I
      17: goto          27
      20: aload_1
      21: invokevirtual #5                  // Method com/example/demo/model/User.getAge:()Ljava/lang/Integer;
      24: invokevirtual #6                  // Method java/lang/Integer.intValue:()I
      27: invokestatic  #7                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      30: astore_3
      31: invokestatic  #8                  // Method com/example/demo/model/User.builder:()Lcom/example/demo/model/User$UserBuilder;
      34: aload_3
      35: invokevirtual #9                  // Method com/example/demo/model/User$UserBuilder.age:(Ljava/lang/Integer;)Lcom/example/demo/model/User$UserBuilder;
      38: invokevirtual #10                 // Method com/example/demo/model/User$UserBuilder.build:()Lcom/example/demo/model/User;
      41: pop
      42: return

重点关注下面这两行:

24: invokevirtual #6 // Method java/lang/Integer.intValue:()I 
27: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

发现还是会先进行一次拆箱,执行Integer的intValue方法,紧接着再进行一次装箱,执行Integer的valueOf方法,最终存储到变量i中。

综合得到以下结论: 三元运算符中:

  • 当第二位和第三位操作数的类型相同时,则三元运算符表达式返回的结果类型和这两位操作数的类型保持一致
  • 当第二和第三位操作数分别为基本类型和该基本类型对应的包装类型时,那么该表达式返回的结果类型默认为基本类型

所以上述代码在运行过程中,始终会会执行一次拆箱,null.intValue()必然会报npe。通过将getUserAge方法的返回类型改为Integer,保证了返回的数据类型一致,成功解决了报错。在日常使用三元运算符时,给出以下建议:

  • 保证返回的数据类型一致,避免内部的类型转换出现问题
  • 必要时可使用if...else...替代三元运算符