写了几年的三元表达式,我怎么还踩坑了?

问题概述

同学们好呀,我是老骥!

跟往常一样,为了代码的优雅就不写太多的 if…else…,于是一顿噼里啪啦的操作,写了一个三元表达式来获取值,一运行竟然报错了。代码如下:

public static void main(String[] args) {
    Integer a = null;
    boolean flag = false;
    Integer b = flag ? 1 : a;
}

代码很简单,就是一个普通的三元表达式,按照之前的预期,当 flag 为 true 时,自动装箱 1 赋值给 b;当 flag 为 flase 时,返回了 null 给 b。但是,运行代码之后就报了空指针异常: Exception in thread "main" java.lang.NullPointerException。可真行,写了多年的三元表达式了,头一次遇到这种情况。

问题分析

那为什么会出现空指针这种情况呢?

我们赋值给 Integer 对象一个 1 或 null 都可以直接运行成功,且直接使用 Object 对象来接收三元表达式的返回值也报了同样的空指针异常,那么问题就出现在了三元表达式上的数据类型转换上了。

三元表达式数据类型转换有以下规则:

  1. 当两个操作数不可互相转换时,则不转换,直接返回 Object 的类型。

    Object b = flag ? new String() : new Integer(1024); 中 String 和 Integer是两种不同类型的对象,不可以互相转换,就直接返回了 Object 类型的数值。

  2. 若存在两个操作数:S、T,S 包含 T,则最终返回 S 类型的值。

    代码示例:

    public static void main(String[] args) {
            Integer a = 1024;
            boolean flag = true;
            Object b1 = flag ? 1 : a;
            Object b2 = flag ? a : 1;
    
            System.out.println("b1: " + b1 + ", b1 instanceof Integer: " + (b1 instanceof Integer));
            System.out.println("b2: " + b2 + ", b2 instanceof Long: " + (b2 instanceof Integer));
    }
    

    运行结果:

    b1: 1, b1 instanceof Integer: true
    b2: 1024, b2 instanceof Long: true
    

    从上述运行结果可以看到,最终返回的 Object 类型都是 Integer 类型,因为 Integer 包含 null 值和 int 类型的值。

  3. 如果两个操作数能够互相转换,则向上转型。

    代码示例:

    public static void main(String[] args) {
            Integer a = 1024;
            boolean flag = true;
            Object b1 = flag ? 1L : a;
            Object b2 = flag ? a : 1L;
            
            System.out.println("b1: " + b1 + ", b1 instanceof Long: " + (b1 instanceof Long));
            System.out.println("b1: " + b1 + ", b1 instanceof Long: " + (b1 instanceof Integer));
            System.out.println("b2: " + b2 + ", b2 instanceof Long: " + (b2 instanceof Long));
            System.out.println("b2: " + b2 + ", b2 instanceof Long: " + (b2 instanceof Integer));
        }
    

    运行结果:

    b1: 1, b1 instanceof Long: true
    b1: 1, b1 instanceof Long: false
    b2: 1024, b2 instanceof Long: true
    b2: 1024, b2 instanceof Long: false
    

    从上述运行结果可以看到,三元表达式返回的 Object 对象都是 Long 类型,即使表达式运算结果返回了值为 Integer 的对象,最终也经过拆箱装箱操作,向上转型为了 Long 类型。

了解了三元表达式的规则,再回过头来看一下一开始报空指针异常的代码:

public static void main(String[] args) {
    Integer a = null;
    boolean flag = false;
    Integer b = flag ? 1 : a;
}

按照上述表达式中的操作数类型,应该使用了第 2 条匹配规则,即返回了范围类型大的 Integer 类型。

我们使用 javap -c Test.class 反编译一下字节码文件。

image-20200803191915834

可以看到,当 a 赋值给 b 的时候,是执行了 a 对象的 Integer.intValue() 先拆箱再进行装箱 Integer.valueOf() 返回一个新的 Integer 对象给 b。所以,当 Integer 类型的对象 a 为 null 时,肯定是无法执行该对象下的 intValue() 方法的,所以也就抛出了空指针异常。

Integer.java 部分源码:

 public final class Integer extends Number implements Comparable<Integer> {
 	private final int value;

    public int intValue() {
        return value;
    }

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
}

那解决上述问题的方法也很简单,只需要保证两个操作数的类型一致即可,代码如下:

public static void main(String[] args) {
        Integer a = null;
        boolean flag = false;
        Integer b = flag ? new Integer(1) : a;

        System.out.println("b1: " + b + ", b1 instanceof Integer: " + (b instanceof Integer));
    }

运行结果:

b1: null, b1 instanceof Integer: false

总结

这次在三元表达式上踩的坑,应该很多人都曾经遇到过。这种不算是语法错误,所以编译器无法捕获到这种异常,而且在运行阶段非 null 的情况下也无法暴露出来,所以在我们在写三元表达式的时候要谨慎一点。

要避免这种情况,还是要打好自己的基础,熟悉三元表达式的数据转换规则。在实现过程中尽量使用同种类型的操作数,同时本地做好各种数据类型的空值测试。


版权声明:本文为weixin_44374482原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。