虽然已有几年开发经验,但是工作中发现,这程序中的坑啊,踩的也真是不亦乐乎,很是惭愧。

即使是最最基础的Java语言本身的特性,也有点儿说不清道不明了。大家都曾努力学习过,都正在努力地工作,为什么一些错误总是一而再再而三的犯呢?我想这不是个人基础的问题,而是没有认真总结与回顾。

所以,即日起,小弟我会把工作上学习中遇到的问题都记录下来,可能有些问题会很白痴,然而不惧贻笑大方方能进步。

这第一个问题便是:

包装类型在条件运算中的使用不当造成的空指针异常。

我们曾在教科书上学到过,条件表达式

b ? x : y;

等价于

if (b) {
  x;
} else {
  y;
}

所以

int a = (b ? x : y);

等价于

int a;
if (b) {
  a = x;
} else {
  a = y;
}

接下来介绍我的发现。

//InnerClass的简化定义如下:
private class InnerClass {
    private Long id;
    
    Long getId() {
        return this.id;
    }
}

private static void testBoxedTypeWithIf() {
    InnerClass innerClass = new InnerClass();
    Long idValue;
    if (flag) {
        idValue = innerClass.getId();
    } else {
        idValue = 0L;
    }
    System.out.println(idValue);
}

flag的值为true,所以程序毫无疑问会打印出null。

根据if判断和条件运算的等价关系,我在程序中用了如下的方式实现逻辑:

private static void testBoxedTypeWithConditionalOperation() {
    InnerClass innerClass = new InnerClass();
    Long idValue = flag ? innerClass.getId() : 0L;
    System.out.println(idValue);
}

如果使用一样的测试用例,肯定有人会认为这段程序也输出null,至少之前的我是这么认为的。这段程序运行的结果是抛出空指针异常,我们来看看testBoxedTypeWithConditionalOperation方法和testBoxedTypeWithIf方法的反编译代码:

private static void testBoxedTypeWithConditionalOperation() {
    InnerClass innerClass = new InnerClass(null);
    Long idValue = Long.valueOf(flag ? innerClass.getId().longValue() : 0L);
    System.out.println(idValue);
}

private static void testBoxedTypeWithIf() {
    InnerClass innerClass = new InnerClass(null);
    Long idValue;
    if (flag)
      idValue = innerClass.getId();
    else {
      idValue = Long.valueOf(0L);
    }
    System.out.println(idValue);
}

可以看到,testBoxedTypeWithConditionalOperation中将条件运算中的包装类型解包装了,在testBoxedTypeWithIf中将原始类型包装了。

在我以往的认知中,包装类型与原始类型的自动包装与解包装的操作,就像testBoxedTypeWithIf中,是在这些变量之间有直接的比较、赋值或者计算(如加减乘除)的时候。然而在本例的条件运算中,即使看似没有直接的关系,编译器依然对包装类型做了解包装的操作。

所以,在包装类型与条件运算混用的时候,要么用if判断替代条件运算,要么在条件运算的判断条件中先对包装类型是否为空做判断,做法如下:

private static void testBoxedTypeWithCorrectConditionalOperation() {
    InnerClass innerClass = new InnerClass();
    Long idValue = flag && innerClass.getId() != null ? innerClass.getId() : 0L;
    System.out.println(idValue);
}

对于本例中用的测试用例,即flag == true,testBoxedTypeWithCorrectConditionalOperation将输出0。

通过反编译代码发现,条件运算与if判断只是在程序的执行路径上保持一致,具体的执行细节,因不同编程语言而异。而今Java前端与后端编译器越来越聪明,对程序员来说不见得是一件好事儿。