三元运算符中,表达式1和2在涉及算术计算或数据类型转换时会触发自动拆箱。当操作数为空值时,会导致NPE。本文将详细分析NPE出现的原因,重新梳理相关知识点,进一步展开,帮助大家全面认识这个问题。近日,《Java 开发手册》发布了最新版本——泰山版。新版本增加了30+个协议,其中一个引起了笔者的注意,就是手册中提到在使用三元运算符的过程中,需要注意NullPointerException(以下简称:NPE)自动拆箱造成的:因为很久以前(2015年)就遇到过这个问题,在博客中也有记录。刚好最新的开发手册又提到了这个知识点,于是把上一篇的内容挖出来重新整理了一下,带大家一起来回顾一下这个知识点。有些人可能已经看过我之前的文章。本文不是简单的“旧瓶装新酒”。笔者在整理这个知识点的时候,重新阅读了《The Java Language Specification》,对比了JavaSE7和JavaSE8,希望能帮助大家更全面的理解这个问题。基础回顾在详细看介绍之前,先简单介绍一下本文涉及的几个重要概念,即“三元运算符”、“自动拆包”等,如果你对这些历史知识有所掌握,可以跳过这一段首先,只看问题重现部分。三元运算符在《The Java Language Specification》,三元运算符的正式名称是ConditionalOperator?:,我一般称他为条件表达式,在JLS15.25中有详细介绍。下面简单介绍一下它的基本形式和用法。三元运算符是Java语言的重要组成部分,它是唯一具有3个操作数的运算符。形式是:<表达式1>?<表达式2>:在<表达式3>之上,可以通过组合?和:。的意思?运算符是:先求表达式1的值,如果为真,则执行并返回表达式2的结果;如果表达式1的值为false,则执行并返回表达式3的结果。值得注意的是,条件表达式永远不会同时计算和。条件运算符是右结合的,即从右到左分组和求值。例如,a?b:c?d:e将被执行为a?b:(c?d:e)。自动装箱和自动拆箱介绍完三元运算符(条件表达式),下面简单介绍一下Java自动拆箱的相关知识点。每个Java开发人员都必须熟悉Java中的基本数据类型。Java中有8种基本数据类型。这些基本数据类型的好处之一是它们直接存储在栈内存中,不会在堆上分配内存。,使用效率更高。但是Java语言是面向对象的语言,基本数据类型不是对象,这在实际使用过程中造成了很多不便。比如集合类要求其内部元素必须是Object类型,不能使用基本数据类型。因此,相应的,Java提供了8种封装类型,在需要对象的地方使用起来更方便。对于基本数据类型和包装类,一个麻烦是需要在它们之间进行转换。在JavaSE5中,为了减少开发者的工作量,Java提供了自动拆箱和自动装箱的功能。自动装箱:就是将基本数据类型自动转换成相应的包装类。自动拆箱:就是将包装类自动转换成对应的基本数据类型。Integeri=10;//自动装箱intb=i;//自动拆箱我们可以简单理解为当我们自己编写的代码符合装箱(拆箱)规范时,编译器会自动帮我们拆箱(装箱)。自动装箱是通过包装类的valueOf()方法实现的。自动拆箱是通过包装类对象的xxxValue()实现的(如booleanValue()、longValue()等)。问题复现最新版本的开发手册给出了一个例子,提示我们在使用三元算子的过程中,可能会进行自动拆箱,导致NPE问题。原文中的例子比较复杂,因为还涉及到多个Integer相乘的结果是int的问题。先举一个比较简单的例子来复现这个问题:booleanflag=true;//设置为true,保证条件表达式的第二个表达式可以执行booleansimpleBoolean=false;//定义一个基本数据类型的boolean变量BooleannullBoolean=null;//定义一个包装类对象类型的布尔变量,值为nullbooleanx=flag?nullBoolean:simpleBoolean;//使用三元运算符,给x变量赋值上面的代码会抛出NPE:Exceptioninthread"main"java.lang.NullPointerException和你使用的JDK版本无关,笔者测试过分别在JDK6、JDK8和JDK14上,所有这些都会抛出NPE。为了一探究竟,我们尝试反编译了上面的代码。使用jad工具反编译后得到如下代码:booleanflag=true;booleansimpleBoolean=false;BooleannullBoolean=null;booleanx=flag?nullBoolean.booleanValue():simpleBoolean;可以看到,在反编译代码的最后一行,编译器已经帮我们做了一次自动拆箱,正是因为这个自动拆箱,代码才出现调用了一个空对象(nullBoolean.booleanValue()),结果在NPE中。那么,编译器为什么要进行自动拆箱呢?什么情况下需要自动拆箱?原理分析关于编辑器为什么在代码编译阶段自动对三元运算符中的表达式进行拆箱,其实在《The Java Language Specification》(以下简称JLS)第15.25章有相关介绍。在不同版本的JLS中,虽然对这部分的描述不尽相同,尤其是在Java8中有了很大的更新,但其核心内容和原理没有改变。直接看JavaSE1.7JLS中对这部分的描述(因为1.7的表达式更简洁):条件表达式的类型判断如下:?如果第二个和第三个操作数的类型相同(可能是null类型),那么这就是条件表达式的类型。?如果第二个和第三个操作数之一是原始类型T,而另一个的类型是对T应用装箱转换(§5.1.7)的结果,则条件表达式的类型是T。简单地说:当第二个和第三个操作数的类型相同时,三元运算符表达式的结果与两个操作数的类型相同。当第二个和第三个操作数为基本类型和基本类型对应的封装类型时,则要求表达式结果的类型为基本类型。为了满足上面的要求,防止程序员过度感知这个规则,如果编译器发现三元运算符的第二个和第三个操作数的类型是基本数据类型(比如boolean)并且是基本类型时对应一个wrapper类型(比如Boolean),返回表达式需要是wrapper类型,那么wrapper类需要自动拆箱。在JavaSE1.8JLS中,对这部分描述做了一些细分,表达式再次分为布尔条件表达式(BooleanConditionalExpressions)、数值条件表达式(NumericConditionalExpressions)和引用类型条件表达式公式(Reference条件表达式)。并且以表格的形式,明确的列出了第二位和第三位不同类型时的表达式结果值应该是什么。如果您有兴趣,可以阅读它。其实简单总结就是:当第二个和第三个表达式都是包装类型时,表达式的结果就是包装类型,否则,只要有一个表达式的类型是基本数据类型,那么得到的结果by表达式都是原始数据类型。如果结果不符合预期,那么编译器将执行自动拆箱。即Java开发手册中总结:只要表达式1和表达式2的类型之一是基本类型,就会进行触发类型对齐的拆箱操作,但如果都是基本类型,则有无需拆箱。下面三种情况是我们在声明表达式结果的类型时熟悉规则并且符合规则的情况(为了帮助大家理解,我把注释和反编译代码都记下来了):booleanflag=true;booleansimpleBoolean=false;BooleanobjectBoolean=Boolean.FALSE;//当第二个和第三个表达式都是对象时,表达式的返回值也是一个对象。Booleanx1=flag?objectBoolean:objectBoolean;//反编译后的代码为:Booleanx1=flag?objectBoolean:objectBoolean;//因为x1的类型是对象,所以不需要做任何特殊操作。//当第二个和第三个表达式都是基本类型时,表达式的返回值也是基本类型。booleanx2=flag?simpleBoolean:simpleBoolean;//反编译后的代码为:booleanx2=flag?simpleBoolean:simpleBoolean;//因为x2的类型也是基本类型,所以不需要做任何特殊操作。//当第二个和第三个表达式其中一个是基本类型时,表达式的返回值也是基本类型。booleanx3=flag?objectBoolean:simpleBoolean;//反编译后的代码为:booleanx3=flag?objectBoolean.booleanValue():simpleBoolean;//因为x3的类型是基本类型,所以包装类需要拆箱。因为我们熟悉三元运算符的规则,所以我们将按照上面的方式定义x1、x2、x3的类型。不过并不是每个人都熟悉这个规则,所以在实际应用中,会有以下三种定义方式://当第二个和第三个表达式都是对象时,表达式的返回值也是一个对象。booleanx4=flag?objectBoolean:objectBoolean;//反编译代码为:booleanx4=(flag?objectBoolean:objectBoolean).booleanValue();//因为x4的类型是基本类型,所以表达式的结果需要自动拆箱。//当第二个和第三个表达式都是基本类型时,表达式的返回值也是基本类型。booleanx5=flag?simpleBoolean:simpleBoolean;//反编译代码为:Booleanx5=Boolean.valueOf(flag?simpleBoolean:simpleBoolean);//因为x5的类型是对象类型,所以表达式结果需要自动装箱。//当第二个和第三个表达式其中一个是基本类型时,表达式的返回值也是基本类型。booleanx6=flag?objectBoolean:simpleBoolean;//反编译后的代码为:Booleanx6=Boolean.valueOf(flag?objectBoolean.booleanValue():simpleBoolean);//因为x6的类型是对象类型,所以表达式的结果需要待计算自动装箱。所以在日常开发中可能会出现以上六种情况。聪明的读者读到这里一定已经想到了。以上六种情况,如果涉及到自动拆箱,一旦对象的值为null,必然会出现NPE。例如验证,我们将上述x3、x4、x6中的对象类型设置为null,分别执行如下代码:nullBoolean:simpleBoolean;以上三种情况,在执行过程中都会出现NPE。其中x3和x6是三元运算符运行时根据JLS规则判断类型的过程中自动拆箱造成的NPE。由于使用了三元运算符,第二个和第三个操作数分别是原始类型和对象。有必要拆箱对象。由于object为null,所以在开箱过程中调用null.booleanValue()会报NPE。而x4是因为三元运算符运算后根据规则得到了一个对象类型,但是NPE是变量赋值过程中自动拆箱造成的。小结上面说了,在开发过程中,如果涉及到三元运算符,那么就要非常注意自动拆箱的问题。最好的办法是让三元运算符的第二个和第三个表达式的类型保持一致,如果要将三元运算符表达式赋值给一个变量,尽量让变量的类型和它们保持一致。并且,做好单元测试!!!所以在《Java 开发手册》中提到,在第二个和第三个表达式的类型对齐过程中,我们要高度重视自动拆箱带来的NPE问题。其实我们还需要注意使用三元运算符表达式给变量赋值时自动拆箱导致的NPE问题。至此,我们就介绍完了《Java 开发手册》关于三元运算符在使用过程中可能导致NPE的问题。如果一定要给出避免这个问题的方法论,那么在使用的过程中,无论是三元运算符中的三个表达式,还是三元运算符表达式要赋值的变量,最好使用包类型,即可以减少出错的概率。延伸思考为了方便大家理解,我用一个简单的布尔类型的例子来说明NPE的问题。然而在实际的代码开发中,遇到的场景可能并没有这么简单。例如下面的代码,请猜猜能否正常执行:Mapmap=newHashMap();Booleanb=(map!=null?map.get("Hollis"):错误的);如果你的回答是“不会,这里会抛出NPE”,那你就明白这篇文章的内容了,但我只能说你只答对了一半。因为以上代码,在JDK1.8以下版本执行结果为NPE,在JDK1.8及以后版本执行结果为null。之所以有这样的差别,说来话长。我简单介绍一下重点。下面的内容主要围绕Java8的JLS展开。在JLS15中,将条件表达式(三元运算符)细分为三种,并做了区分:如果表达式的第二个和第三个操作数是布尔表达式,那么条件表达式就是一个布尔表达式。如果表达式的第二个和第三个操作数都是数值表达式,则条件表达式是一个数值表达式。除了上面两个之外的表达式都是引用表达式,因为Booleanb=(map!=null?map.get("Hollis"):false);表达式中,第二个操作数是map.get("test"),虽然Map在定义的时候规定了它的值类型是Boolean,但是在编译的时候泛型会被擦除(generictypeerasure),所以结果是目的。那么根据上面的规则,这个表达式就是一个引用表达式。同样根据JLS15.25.3:如果引用条件表达式出现在赋值上下文或调用上下文中,则条件表达式是复合表达式,因为Booleanb=(map!=null?map.get("Hollis"):错误的);实际上是一个赋值上下文(参见JLS5.2的赋值上下文),所以map!=null?map.get("霍利斯"):false;是一个复合表达式。那么JLS15.25.3对复合表达式的操作数类型做了约束:复合引用条件表达式的类型与其目标类型相同所以,因为这个约束,编译器可以推断(Java8中的类型推断,详见JLS18,这个表达式的第二个操作数和第三个操作数的结果都应该是Boolean类型,因此在编译过程中,可以将它们分别转换成Boolean,然后上面的代码在Java8中被反编译内容如下:Booleanb=maps==null?Boolean.valueOf(false):(Boolean)maps.get("Hollis");但是Java7中没有这样的规定(Java8之前的类型推断函数)仍然很弱),编译器只知道表达式的第二和第三位是基本类型和包装类型,而不能推断出最终的表达式类型。那么他会先把返回值result按照JLS15.25的规定转换为基本类型。然后,在分配变量时,将它们转换为包装类型:Booleanb=Boolean.valueOf(maps==null?false:((Boolean)maps.get("Hollis")).booleanValue());所以,相比Java8,多了一个自动拆箱的步骤,所以会造成NPE。参考资料:【1】《Java 开发手册(泰山版)》【2】http://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.25【3】http://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.25【4】https://docs.oracle.com/javase/specs/jls/se8/html/jls-15。html#jls-15.2【5】https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.2.7【6】https://docs.oracle.com/javase/specs/jls/se8/html/jls-18.html