三目运算符潜规则

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/caonima0001112/article/details/50363904

引言

三目运算符是我们经常在代码中使用的,a= (b==null?0:1); 这样一行代码可以代替一个 if-else,可以使代码变得清爽易读。但是,三目运算符也是有一定的语言规范的。在运用不恰当的时候会导致意想不到的问题。前段时间遇到(一个由于使用三目运算符导致的问题,其实是因为有三目运算符和自动拆箱同时使用(虽然自动拆箱不是我主动用的)。

三目运算符

对于条件表达式b?x:y,先计算条件b,然后进行判断。如果b的值为true,计算x的值,运算结果为x的值;否则,计算y的值,运算结果为y的值。一个条件表达式从不会既计算x,又计算y。条件运算符是右结合的,也就是说,从右向左分组计算。例如,a?b:c?d:e将按a?b:(c?d:e)执行。

自动装箱与自动拆箱

基本数据类型的自动装箱(autoboxing)、拆箱(unboxing)是自J2SE 5.0开始提供的功能。 一般我们要创建一个类的对象实例的时候,我们会这样: Class a = new Class(parameters); 当我们创建一个Integer对象时,却可以这样: Integer i = 100;(注意:和 int i = 100;是有区别的 ) 实际上,执行上面那句代码的时候,系统为我们执行了: Integer i = Integer.valueOf(100); 这里暂且不讨论这个原理是怎么实现的(何时拆箱、何时装箱),也略过普通数据类型和对象类型的区别。我们可以理解为,当我们自己写的代码符合装(拆)箱规范的时候,编译器就会自动帮我们拆(装)箱。那么,这种不被程序员控制的自动拆(装)箱会不会存在什么问题呢?

其所以然

最近发现了一个很诡异的NullPointerException,在下面这个方法抛出,一开始怎么都没想明白,dSrc即使为null,那直接赋值给distinct也没问题啊。

  private Doubledistinct;

  private void setParam(Double dSrc, boolean flag) {

  this.distinct = (flag) ? dSrc : 0d;

  }

  最后才发现是Java自动拆箱的潜规则,下面我们来看看其所以然。

自动装箱/拆箱

  在JDK1.5引入自动装箱/拆箱,提高了我们的开发效率,也让我们的代码变得更加简洁,不用显式转换:

  Double dWrap1 = 10d;

  double d1 = dWrap1;

  double d2 = d1 + dWrap1;

  DoubledWarp2 = d2 + dWrap1;

  实际上,自动装箱/拆箱是通过编译器来支持的,JVM并没有改变。我们反编译能看到上面的源码会变成:

  Double dWrap1 = Double.valueOf(10.0d);

  double d1 = dWrap1.doubleValue();

  double d2 = d1 + dWrap1.doubleValue();

  DoubledWarp2 = Double.valueOf(d2 + dWrap1.doubleValue());

  编译器的意图很明显,帮我们完成基本类型和封装类型的相互转换;另外,对于封装类的运算中,先要转换成基本类型,再进行计算。

  但是,这么自动转换,问题就来了,如果我把dWrap1初始化为null,再赋值给d1,相当于把null赋值给了基本类型double.编译的时候是没有问题的,因为编译器还认为是封装类Double类型,会帮我们自动拆箱赋值给d1,只是运行的时候会抛NullPointerException,如下:

  Double dWarp1 = null;

  double d1 = dWarp1;

  这其实是个很低级的bug,很容易防范,加个非空校验就可以避免。一般原则也是,在使用每个Object之前都要做非空校验,一些代码检查工具也会帮我们做这个校验,如FindBugs.所以我们可以写成以下形式:

  Double dWarp1 = null;

  double d1 = 0d;

  if (null != dWarp1) {

  d1 = dWarp1;

  }

三目运算的潜规则

  有时候,我们为了代码的简洁性,会引入三目运算符:

  double d1 = (null != dWarp1) ? dWarp1 : 0d;

  但是,也有比较诡异的情况:根据条件flag判断,如果true则赋值dWarp1,否则设为默认值0,如下。

  Double dWarp1 = null;

  boolean flag =true;

  DoubledWarp2 = (flag) ? dWarp1 : 0d;

  这乍眼一看,很正常嘛,相当于dWarp2 = dWarp1,但是运行起来却会抛异常NullPointerException.

  这就是编译器的自动装箱/拆箱转换引起的问题。我们反编译就能看到,原来他对dWarp1做了一层拆箱,这样就出现前面我们所说的问题,如果dWarp1为null的话,就挂了。

  DoubledWarp2 = Double.valueOf((flag) ? dWarp1.doubleValue() : 0.0D);

其实,这是自动装箱/拆箱的特性,只要一个运算中有不同的类型,涉及到类型转换,那么编译器会往下(基本类型)转型,再进行运算。 就是说,如果运算中有int和Integer,Integer会先转成int再计算。

  所以下面这种写法照样会抛出NullPointerException:

  Double dWarp1 = null;

  Long l2 = null;

  boolean flag =false;

  DoubledWarp2 = (flag) ? dWarp1 : l2;

  因为dWarp1和l2都要先转换成基本类型,而不是互相转换,反编译后变成:

  Double dWarp2 = Double.valueOf((flag)? dWarp1.doubleValue() : l2.longValue());
  

因此,这里可以改成下面三种方式:

  1.在赋值前,先做非空校验,但是这样做比较繁琐,因为很多时候dWarp2是可以接受null的,这个非空判断只是用来避免编译器的自动拆箱异常。

  2.避免使用三目运算符,不过估计会有很多人不忍抛弃三目(我也算一个)。

  Double dWarp1 = null;

  boolean flag =true;

  Double dWarp2 = 0d;

  if (flag) {

  dWarp2 = dWarp1;

  }

  3.统一运算中的类型,避免类型的混用了。(个人觉得这种比较优雅)

  Double dWarp1 = null;

  boolean flag =true;

  DoubledWarp2 = (flag) ? dWarp1 : Double.valueOf(0);

猜你喜欢

转载自blog.csdn.net/caonima0001112/article/details/50363904