(1)小数之谜
猜猜看,下面这段代码的输出结果是什么?
publicclassAboutExpressions {publicstaticvoidmain(String[] args){
System.out.println(2.00-1.10);
}
}
答案不是你期望的0.90,而是一个很奇怪的数字:0.8999999999999999
问题就在于1.1这个数字不能被精确地表示为一个double,因此被表示为最接近它的double值,该程序从2中减去的就是这个值。更一般地说,并不是所有的小数都可以用二进制浮点数精确表示。
解决方法:使用执行精确小数运算的BigDecimal,注意一定要使用BigDecimal(String),而不能用BigDecimal(double),后者将用它的参数的精确值来创建一个实例,你可以试试看new BigDecimal(.1)返回的是什么。
将输出语句改为System.out.println(new BigDecimal("2.00").subtract(new BigDecimal("1.10")));即可得到期望的答案0.90。
总之,在需要精确答案的地方,要避免使用float和double;对于货币计算,要使用int、long或BigDecimal
(2)长整除之谜
下面的程序中被除数表示一天里的微秒数,除数是一天里的毫秒数,long类型可以保证这两个乘积不溢出,猜猜看,程序运行的结果是什么?
publicclassAboutExpressions {publicstaticvoidmain(String[] args){finallongMICROS_PER_DAY=24*60*60*1000*1000;finallongMILLIS_PER_DAY=24*60*60*1000;
System.out.println(MICROS_PER_DAY/MILLIS_PER_DAY);
}
}
看上去,程序必定输出1000,遗憾的是,输出的结果却是5。
问题在与MICROS_PER_DAY的计算确实溢出了,因为这个运算完全是以int运算来执行的,并且只有在运算完成、产生了溢出之后,才把结果转换成long。
解决方法很简单:
final long MICROS_PER_DAY=24L*60*60*1000*1000;
final long MILLIS_PER_DAY=24L*60*60*1000;
这样可以强制表达式中所有的后续计算都以long运算来完成。当我们在操作很大的数字时,千万要提防溢出。
(3)看完前两个之后后,你肯定对数字变得非常敏感,那看看下面这行代码会输出什么?
publicclassAboutExpressions {publicstaticvoidmain(String[] args){
System.out.println(12345+5432l);
}}
这个问题的答案很恶心,它不是66666,而是17777。
因为这里是将一个int类型的12345加到了long类型的5432l上,注意5432后的l不是数字1,而是小写字母l
所以在long类型的字面常量中,一定要用大写的L,而不是小写的l
(4)多重转换之谜。
下面这段代码连续用了3次类型转换,它最终会回到起点,打印出-1吗?
publicclassAboutExpressions {publicstaticvoidmain(String[] args){
System.out.println((int)(char)(byte)-1);
}}
结果并非我们期望的-1,而是65535。
该程序与Java类型转换的符号扩展行为紧密相关。Java使用了基于2的补码的二进制运算,因此int类型的数值-1的所有32位都是置位的。
int-->byte:
从int到byte实行的是窄化原生类型转换(narrowing primitive conversion),直接将低八位以外的所有位全部去掉,得到的是8位都置位的byte 11111111,仍旧表示-1。
byte-->char:
byte到char的转型稍麻烦,因为byte是有符号类型,char是无符号类型,我们不能用一个char表示一个负的byte数值。因此,从byte到char是一种拓宽并窄化原生类型的转换(widening and narrowing primitive conversion),byte(11111111)被转化为int(32位1),而这个int又被转化为char(16位1),char是无符号的,所以它表示2^16-1=65535。
有一条简单的规则能够描述从较窄的整型转换成较宽的整型时的符号扩展行为:如果最初的数值类型是有符号的,那么就执行符号扩展;如果它是char,那么不管它将要被转换成什么类型,都执行零扩展。
char-->int:
char到int是一个拓宽原生类型转换,正如上面这条所述,它执行零扩展,结果是00000000 00000000 11111111 11111111,即65535
(5)条件操作符之谜
publicclassAboutExpressions {publicstaticvoidmain(String[] args){charx='X';inti=0;
System.out.print(true?x:0);
System.out.print(false?i:x);
}}
看上去,输出结果应该是XX,而真正的结果却是X88;
在这两个条件表达式之中,x是char型的,0和i都是int型的,混合类型的计算会引起混乱,这一点在条件表达式之中表现得更为明显。
如何确定条件表达式结果的类型呢?有三条核心规则:
(1)如果第二个和第三个操作数具有相同的类型,那么它就是条件表达式的类型。
(2)如果第一个操作数的类型是T,T表示byte、char或short,而另一个操作数是一个int类型的常量表达式,且它的值可以用类型T表示,那么条件表达式的类型就是T。
(3)否则,将对操作数类型进行二进制数字提升,而条件表达式的类型就是第二个和第三个操作数被提升之后的类型。
System.out.print(true?x:0);其中x是char型,0是int型常量,符合第二条规则,因此返回的类型是char。
System.out.print(false?i:x);其中i是int型变量,不满足第二条规则,其返回的类型是对int和char进行二进制提升之后的类型,即int
若想程序输出XX,可以将i声明为final int i=0,把i转换成常量表达式。
总之,通常最好是在条件表达式中使用类型相同的第二和第三操作数。