记一次Java三目运算导致的空指针异常

189 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情

首先上代码:

public class A {

    private Long age;

    public Long getAge() {
        return age;
    }

    public void setAge(Long age) {
        this.age = age;
    }
}

这是一个很普通的一个类,里面有一个Long型的age 属性。 再看一下业务代码:

public static void main(String[] args) {
    A a = new A();

    Long age = (a == null ? 0L : a.getAge());
    System.out.println("age = " + age);
}

这个代码就是判断a对象是否为空,如果为空就返回0L,否则就取a对象的age属性值。这种代码应该来说在我们业务开发中会见到。但是...... 运行一下看结果:

image.png 当时我就奇怪了,a是判空了的呀,如果a为空怎么会报空指针异常呢?况且a在这里不为空,唯一为null的是a里面的age属性,此时我就怀疑是JVM搞的鬼。 通过对class文件反汇编看看:执行以下javap -c 试试看:

public class com.example.demo.bugs.Ternary {
  public com.example.demo.bugs.Ternary();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class com/example/demo/bugs/A
       3: dup
       4: invokespecial #3                  // Method com/example/demo/bugs/A."<init>":()V
       7: astore_1
       8: aload_1
       9: ifnonnull     16
      12: lconst_0
      13: goto          23
      16: aload_1
      17: invokevirtual #4                  // Method com/example/demo/bugs/A.getAge:()Ljava/lang/Long;
      20: invokevirtual #5                  // Method java/lang/Long.longValue:()J
      23: invokestatic  #6                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
      26: astore_2
      27: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      30: new           #8                  // class java/lang/StringBuilder
      33: dup
      34: invokespecial #9                  // Method java/lang/StringBuilder."<init>":()V
      37: ldc           #10                 // String age =
      39: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      42: aload_2
      43: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      46: invokevirtual #13                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      49: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      52: return
}

注意看字节码的20行号,发现了一个Long.valueOf的优化指令,猜测JVM在对字面量形式的Java代码做了编译优化,三目运算里面自动优化成了Long.valueOf(a.getAge())

我们试一下这种写法会有什么结果:

public static void main(String[] args) {
    A a = new A();

    // Long age = (a == null ? 0L : a.getAge());
    // System.out.println("age = " + age);

    System.out.println("test" + Long.valueOf(a.getAge()));
}

image.png 果然,问题的原因找到了,是Jvm在对一些字面量形式的代码会做一些编译优化。

常见的优化比如Long.valueOf、Integer.valueOf、StringBuilder等等

最后解决方案就是:

public static void main(String[] args) {
    A a = new A();

    Long age = (a == null ? Long.valueOf(0L) : a.getAge());
    System.out.println("age = " + age);
}

避免以字面量形式进行三目运算比较,否则JVM会做编译期动态优化。