Java教程

【Java面试进阶】谈一谈为什么使用三目运算符时必须要注意类型对齐?

本文主要是介绍【Java面试进阶】谈一谈为什么使用三目运算符时必须要注意类型对齐?,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

谈一谈为什么使用三目运算符时必须要注意类型对齐?

  • 三目运算符
  • 自动装箱和自动拆箱
  • 问题重现
  • 原理分析
  • 小结
  • 扩展思考

首先学习一下阿里巴巴的Java开发手册:
Java开发手册关于三目运算符
这里涉及到了几个重要的概念,分别是"三目运算符",”自动拆装箱“。

三目运算符

在《The Java Language Specification》中,三目运算符的官方名称是 Conditional Operator ? :,一般称呼为条件表达式,详细介绍在JLS 15.25中,这里简单介绍下其基本形式和用法:

三目运算符是Java语言中的重要组成部分,它也是唯一有3个操作数的运算符。形式为:

 <表达式1> ? <表达式2> : <表达式3>

这里通过?、:组合的形式得到一个条件表达式。其中?运算符的含义是:先求表达式1的值,如果为真,则执行并返回表达式2的结果;如果表达式1的值为假,则执行并返回表达式3的结果。

值得注意的是,一个条件表达式从不会既计算<表达式2>,又计算<表达式3>。条件运算符是右结合的,也就是说,从右向左分组计算。例如,a?b:c?d:e将按a?b:(c?d:e)执行。

自动装箱和自动拆箱

Java中共有8种基本数据类型,这些基础数据类型带来一个好处就是他们直接在栈内存中存储,不会在堆上分配内存,使用起来更加高效。

但是,Java语言是一个面向对象的语言,而基本数据类型不是对象,导致在实际使用过程中有诸多不便,如集合类要求其内部元素必须是Object类型,基本数据类型就无法使用。

所以,相对应的,Java提供了8种包装类型,更加方便在需要对象的地方使用。

有了基本数据类型和包装类,带来了一个麻烦就是需要在他们之间进行转换。在Java SE5中,为了减少开发人员的工作,Java提供了自动拆箱自动装箱功能。

  • 自动装箱:就是将基本数据类型自动转换成对应的包装类
  • 自动拆箱:就是将包装类自动转换成对应的基本数据类型
Integer i =10;  //自动装箱

int b= i;     //自动拆箱

可以简单理解为,当我们自己写的代码符合装(拆)箱规范的时候,编译器就会自动帮我们拆(装)箱。

自动装箱都是通过包装类的valueOf()方法来实现的.自动拆箱都是通过包装类对象的xxxValue()来实现的(如booleanValue()、longValue()等)。

问题重现

举一个相对简单的一点的例子先来重现下这个问题:

    boolean flag = true; //设置成true,保证条件表达式的表达式二一定可以执行

    boolean simpleBoolean = false; //定义一个基本数据类型的boolean变量

    Boolean nullBoolean = null;//定义一个包装类对象类型的Boolean变量,值为null

    

    boolean x = flag ? nullBoolean : simpleBoolean; //使用三目运算符并给x变量赋值

以上代码,在运行过程中,会抛出NPE:

    Exception in thread "main" java.lang.NullPointerException

我们尝试对以上代码进行反编译,使用jad工具进行反编译后,得到以下代码:

    boolean flag = true;

    boolean simpleBoolean = false;

    Boolean nullBoolean = null;

    boolean x = flag ? nullBoolean.booleanValue() : simpleBoolean;

可以看到,反编译后的代码的最后一行,编译器帮我们做了一次自动拆箱,而就是因为这次自动拆箱,导致代码出现对于一个null对象(nullBoolean.booleanValue())的调用,导致了NPE。

那么,为什么编译器会进行自动拆箱呢?什么情况下需要进行自动拆箱呢?

原理分析

关于为什么编辑器会在代码编译阶段对于三目运算符中的表达式进行自动拆箱,其实在《The Java Language Specification》(后文简称JLS)的第15.25章节中是有相关介绍的。直接看Java SE 1.7 JLS中关于这部分的描述:

The type of a conditional expression is determined as follows: • If the second and third operands have the same type (which may be the null type),then that is the type of the conditional expression. • If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T.

简单的来说就是:当第二位和第三位操作数的类型相同时,则三目运算符表达式的结果和这两位操作数的类型相同当第二,第三位操作数分别为基本类型和该基本类型对应的包装类型时,那么该表达式的结果的类型要求是基本类型

为了满足以上规定,又避免程序员过度感知这个规则,所以在编译过程中编译器如果发现三目操作符的第二位和第三位操作数的类型分别是基本数据类型(如boolean)以及该基本类型对应的包装类型(如Boolean)时,并且需要返回表达式为包装类型,那么就需要对该包装类进行自动拆箱。

在Java SE 1.8 JLS中,关于这部分描述又做了一些细分,再次把表达式区分成布尔型条件表达式(Boolean Conditional Expressions)、数值型条件表达式(Numeric Conditional Expressions)和引用类型条件表达式(Reference Conditional Expressions)。

简单总结下就是:当第二位和第三位表达式都是包装类型的时候,该表达式的结果才是该包装类型,否则,只要有一个表达式的类型是基本数据类型,则表达式得到的结果都是基本数据类型。如果结果不符合预期,那么编译器就会进行自动拆箱

小结

在开发过程中,如果涉及到三目运算符,那么就要高度注意其中的自动拆装箱问题。最好的做法就是保持三目运算符的第二位和第三位表达式的类型一致,并且如果要把三目运算符表达式给变量赋值的时候,也尽量保持变量的类型和他们保持一致。并且,做好单元测试!!!

所以,Java开发手册中提到要高度注意第二位和第三位表达式的类型对齐过程中由于自动拆箱发生的NPE问题,其实还需要注意使用三目运算符表达式给变量赋值的时候由于自动拆箱导致的NPE问题

如果一定要给出一个方法论去避免这个问题的话,那么在使用的过程中,无论是三目运算符中的三个表达式,还是三目运算符表达式要赋值的变量,最好都使用包装类型,可以减少发生错误的概率

扩展思考

实际在代码开发中,遇到的场景可能并没有那么简单,比如说以下代码,猜一下能否正常执行:

    Map<String,Boolean> map =  new HashMap<String, Boolean>();

    Boolean b = (map!=null ? map.get("Hollis") : false);

答案是:以上代码,在小于JDK 1.8的版本中执行的结果是NPE,在JDK 1.8 及以后的版本中执行结果是null。

之所以会出现这样的不同,这个就说来话长了,以下内容主要内容还是围绕Java 8 的JLS 。JLS 15中对条件表达式(三目运算符)做了细分之后分为三种,区分方式:

  • 如果表达式的第二个和第三个操作数都是布尔表达式,那么该条件表达式就是布尔表达式
  • 如果表达式第二个和第三个操作数都是数字型表达式,那么该条件表达式就是数字型表达式。
  • 除了以上两种以外的表达式就是引用表达式

因为Boolean b = (map!=null ? map.get("Hollis") : false);表达式中,第二位操作数为map.get("test"),虽然Map在定义的时候规定了其值类型为Boolean,但是在编译过程中泛型是会被擦除的(泛型的类型擦除),所以,其结果就是Object。那么根据以上规则判断,这个表达式就是引用表达式。

又跟据JLS15.25.3中规定:

  • 如果引用条件表达式出现在赋值上下文或调用上下文中,那么条件表达式就是合成表达式

因为,Boolean b = (map!=null ? map.get("Hollis") : false);其实就是一个赋值上下文(关于赋值上下文相见JLS 5.2),所以map!=null ? map.get("Hollis") : false;就是合成表达式。

那么JLS15.25.3中对合成表达式的操作数类型做了约束:

  • 合成的引用条件表达式的类型与其目标类型相同

所以,因为有了这个约束,编译器就可以推断(Java 8 中类型推断,详见JLS 18)出该表达式的第二个操作数和第三个操作数的结果应该都是Boolean类型。

所以,在编译过程中,就可以分别把他们都转成Boolean即可,那么以上代码在Java 8中反编译后内容如下:

    Boolean b = maps == null ? Boolean.valueOf(false) : (Boolean)maps.get("Hollis");

但是在Java 7中可没有这些规定(Java 8之前的类型推断功能还很弱),编译器只知道表达式的第二位和第三位分别是基本类型和包装类型,而无法推断最终表达式类型。

那么会先根据JLS 15.25的规定,把返回值结果转换成基本类型。然后在进行变量赋值的时候,再转换成包装类型:

    Boolean b = Boolean.valueOf(maps == null ? false : ((Boolean)maps.get("Hollis")).booleanValue());

所以,相比Java 8中多了一步自动拆箱,所以会导致NPE。

参考资料
-《Java开发手册——泰山版》

  • 原文链接:https://developer.aliyun.com/article/757443?spm=a2c6h.12873639.0.0.4d6555ef3nGFnj
这篇关于【Java面试进阶】谈一谈为什么使用三目运算符时必须要注意类型对齐?的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!