《java解惑》是Google公司的首席Java架构师Joshua Bloch继《Effective java》之后有一力作,专门揭示了很多java编程中意想不到的疑惑,很多有多nian工作经验的java开发人员在看完本书之后甚至都怀疑zi己会不会写java程序,本系列博客主要记录在读《java解惑》中的经典例子以及原因分析。

1.奇偶性判断:

问题:

如果使用下面的程序判断整数奇偶性会有什么问题:

public static boolean isOdd(int i){
        return i % 2 == 1;
}
public static boolean isOdd(int i){
        return i % 2 == 1;
}

上述代码对于正整数没有任何问题,但是对于所有负奇数的判断全部都是错误的。

原因:

java对于取余运算符(%)的定义为:对于所有int数值a和所有非零int数值b,都满足如下恒等式:

(a / b) * b + (a % b) == a

当取余操作返回一个非零的结果时,它与左操作数具有相同的正负号,因此负整数模2的余数总数-1而非1.

解决办法:

 (1).一般最常用的解决方法为:

public static boolean isOdd(int i){
        return i % 2 == 0;
}
public static boolean isOdd(int i){
        return i % 2 == 0;
}

(2).对于性能要求的系统,可以使用如下的解决方法:

public static boolean isOdd(int i){
        return (i & 1) != 0;
}
public static boolean isOdd(int i){
        return (i & 1) != 0;
}

2.浮点数精确运算:

浮点数运算时结果是近似值,如果需要使用精确值如金融ji算方面,考虑使用BigDecimal。

注意:new BigDecimal(.9)其实还是浮点数,精确数使用new BigDecimal(".9")。

3.长整除运算:

问题:

下面的程序ji算一天的微秒数除以毫秒数:

public static void main(String[] args) {
  final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;
  final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000;
  System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY);
 }
public static void main(String[] args) {
  final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;
  final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000;
  System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY);
 }

我们期望输出的结果是1000,但是运行的真正结果是5.

原因:

除数和被除数都是long型的,不会产生溢出的问题,但是由于除数和被除数都是以int类型ji算出来的,MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000ji算过程中中间结果仍然以int类型来表示,只在最后的ji算结果才被提升扩展为long类型,而int类型存放MICROS_PER_DAY在类型转换之前会溢出。

解决办法:

明白原因之后,解决办法就很简单,只需要在ji算时明确显式地指定ji算因子为long类型,代码如下:

这个问题的教训是如果在ji算比较大的数值时,ji算因子最好限制转换为扩展后的类型以防止溢出。

public static void main(String[] args) {
  final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
  final long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
  System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY);
 }
public static void main(String[] args) {
  final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
  final long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
  System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY);
 }

4.十六进制混合运算:

问题:

下面一段程序演示两个十六进制数运算:

public static void main(String[] args) {
  System.out.println(Long.toHexString(0x100000000L + 0xCAFEBABE));
 }
public static void main(String[] args) {
  System.out.println(Long.toHexString(0x100000000L + 0xCAFEBABE));
 }

期望结果为:1CAFEBABE,但是运行的真正结果为:CAFEBABE,第33未的1丢失了。

原因:

java中十进制字面常量都是正的,对于负数必须要显示添加负号(-),但是对于二进制,八进制和十六进制来说字面常量的正负数由最高位的符号位表示,如果最高位是1就表示负数,最高位为0就表示整数。

对于long类型来说,能表示的最大十六进制数为:FFFFFFFFFFFFFFFF,0x100000000L补全位数之后为:0000000100000000,最高位为0,表示为正数。

对于int类型来说,能表示的最大十六进制数为:FFFFFFFF,0xcafebabe补全位数之后仍然是CAFEBABE,十六进制和二进制转换是1为十六进制数对应4为二进制数,C对应为1100,因此0xCAFEBABE的最高位为1,其实是负数。

在上述运算中,左操作数是long类型,有操作数是int类型,在ji算时java会zi动对int类型的数进行类型提升扩展为long类型,由于最高位为1,因此在类型提升时需要保持符号位,提升之后0xCAFEBABE变为0xFFFFFFFFCAFEBABE,运算过程如下:

    0x0000000100000000

 +0xFFFFFFFFCAFEBABE

-----------------------------------------

= 0x00000000CAFEBABE

解决办法:

该问题的根本原因在于int类型想long类型提升扩展时符号位的保持扩展造成的,解决办法很简单,只需要将int类型的0xCAFEBABE变为long类型,避免符号位扩充即可,代码如下:

public static void main(String[] args) {
  System.out.println(Long.toHexString(0x100000000L + 0xCAFEBABEL));
 }
public static void main(String[] args) {
  System.out.println(Long.toHexString(0x100000000L + 0xCAFEBABEL));
 }

 5.多重类型转换:

问题:

下面代码输出结果是什么:

public static void main(String[] args) {
  System.out.println((int)(char)(byte)-1);
 }
public static void main(String[] args) {
  System.out.println((int)(char)(byte)-1);
 }

很多人认为输出的结果应该是-1,但是真正运行输出的结果是65535。

原因:

java使用了二进制补码运算,数据类型转换依赖与符号位扩展:

int类型的-1的所有32为都是置位的(高位和符号位全部置位为1),当从32为的int转型到8为的byte时,执行一个窄化原始类型转换,直接保留低8位,得到的结果是8个全是1的二进制数,表示的仍旧是-1。

byte,int,long,short都是有符号类型,而char是一个无符号类型,在将一个8位的有符号byte转换为一个16位的无符号char类型时,首先将8位全是1的byte数进行符号位扩展变成16位全是1的char类型,由于char类型是无符号位,因此得到的结果是一个16为全是1的无符号数,十进制是正的65536。

16位无符号的char类型转换为32位有符号的int类型时,只保留数值,不进行符号位扩展,因此的得到是一个高16位为0,低16位为1的int数,即65536.

结论:

char是仅有的无符号整数,在进行类型转换时要特别小心,如果进行char类型向更宽数据类型转换为请注意以下两条规则:

(1).如果将char类型c向更宽类型int转换时不希望有符号扩展,请使用以下两种方法:

使用位掩码:int i = c & 0xffff;

直接赋值:int i = c;

(2).如果将char类型c向更宽类型int转换时希望有符号扩展,请先将char转换为同样宽度且有符号的short类型:

int i = (short)c;

如果将byte类型数据b向char类型转换时,不希望符号扩展,则考虑使用符号位掩码:

char c = (char) (0xff & b);

6.不使用临时变量进行两个数交换:

正常的使用临时变量交互两个变量值的例子代码如下:

public static void main(String[] args) {
  int x = 1984;
  int y = 2001;
  int tmp = x;
  x = y;
  y = tmp;
  System.out.println("x=" + x + ",y=" + y);
 }
public static void main(String[] args) {
  int x = 1984;
  int y = 2001;
  int tmp = x;
  x = y;
  y = tmp;
  System.out.println("x=" + x + ",y=" + y);
 }

整个程序正常运行结果为:x=2001,y=1984。

在C/C++中,很多人经常使用异或运算来进行两个数交互,达到不使用临时变量的目的,使用java重写的代码如下:

public static void main(String[] args) {
  int x = 1984;
  int y = 2001;
  x ^= y ^= x ^= y;
  System.out.println("x=" + x + ",y=" + y);
 }
public static void main(String[] args) {
  int x = 1984;
  int y = 2001;
  x ^= y ^= x ^= y;
  System.out.println("x=" + x + ",y=" + y);
 }

但是在Java中,运行结果为:x=0,y=1984。

原因:

在以前ji算机硬件比较落后的情况下,CPU只有少数的寄存器,通过利用异或操作符的属性(x ^ y ^ x)== y到达避免使用临时变量的目的,具体运算过程分解如下:

x = x ^ y;

y = y ^ x;// y = y ^ (x ^ y) = x;

x = y ^ x;// x = x ^ (x ^ y) = y;

java语言规范中操作符的操作数是从左向右求值的,为了求表达式x ^= expr的值,x的值是在ji算expr之前被提取的,并且这两个值的异或结果被赋值给变量x。

对于表达式x ^= y ^= x ^= y,x的值被提取两次,每次在表达式中出现时都被提取一次,但是两次提取都发生在所有的赋值操作前,详细分解如下:

int tmp1 = x;//x在表达式中第一次出现

int tmp2 = y;//y在表达式中第一次出现

int tmp3 = x ^ y;//ji算最左边的x ^ y

x = tmp3;//最后一个赋值:将x ^ y存储在x中

y = tmp2 ^ tmp3;//第二个赋值:将y ^ (x ^ y)即原始的x值存储到y中

x = tmp1 ^ y;//第一个赋值:将x ^ x为0的值存储到x中

在C/C++中没有指定表达式的ji算顺序,当编译表达式x ^= expr时,许多C/C++编译器都是在ji算expr之后才提取x的值,因此可以正常工作。

结论:

如果想在java中不是有临时变量交换两个数的值,可以使用如下的代码:

public static void main(String[] args) {
  int x = 1984;
  int y = 2001;
  y = (x ^= (y ^= x)) ^ y;
  System.out.println("x=" + x + ",y=" + y);
 }
public static void main(String[] args) {
  int x = 1984;
  int y = 2001;
  y = (x ^= (y ^= x)) ^ y;
  System.out.println("x=" + x + ",y=" + y);
 }

不过不推荐这种用法,代码读起来不直观明白,并且运行速度也不见得比使用临时变量速度快。

7.三目运算符表达式:

问题:

下面一段程序代码的的输出结果应该是什么?

public static void main(String[] args) {
  char x = 'X';
  int i = 0;
  System.out.println(true ? x : 0);
  System.out.println(true ? x : 65536);
  System.out.println(true ? x : i);
  System.out.println(false ? 0 : x);
  System.out.println(false ? i : x);
 }
public static void main(String[] args) {
  char x = 'X';
  int i = 0;
  System.out.println(true ? x : 0);
  System.out.println(true ? x : 65536);
  System.out.println(true ? x : i);
  System.out.println(false ? 0 : x);
  System.out.println(false ? i : x);
 }

我们期望的输出是:

X
X
X
X
X
X
X
X
X
X

但是真正输入的结果是:

X
88
88
X
88
X
88
88
X
88

原因:

之所以造成奇怪的输出结果原因是三目运算符条件表达式对于第二个第三个操作数类型的规范如下:

(1).如果第二个和第三个操作数具有相同的类型,则该类型就是条件表达式的类型。

(2).如果两个操作数的一个操作数的类型是T,T表示byte,short或者char类型,而另一个操作数是int类型的常量表达式,且该常量值是可以用T表示的,那么条件表达式的值类型就是T。

(3).否则,将对操作数类型进行数据类型扩展提升,而条件表达式值的类型就是第二个和第三个操作数类型被扩展提升后的类型。

通过上面三条规范,我们就很容易明白上面程序的输出结果了:

第1和第4个输出:.由于0是int类型常量,且可以被char类型表示,适用于第二条规范,所以第一个输入就char类型的X.

第2个输出:.65536虽然是int类型的常量,但是超出了char类型的表示范围,适用于第三条规范,因此将类型提升为int,char类型的X提升为int之后值为88.

第3和第5个输出:.i是int类型的变量,适用于第三条规范,因此将类型提升为int,char类型的X提升为int之后值为88.

结论:

写程序中,最好在条件表达式中使用类型相同的第二个和第三个操作数,否则可能输出令你意想不到的结果。

8.复合赋值表达式不等价于简单赋值表达式:

问题:

下面的4行程序哪一行有错:

public static void main(String[] args) {
  short x = 0;//1
  int i = 123456;//2
  x = x + i;//3
  x += i;//4
 }
public static void main(String[] args) {
  short x = 0;//1
  int i = 123456;//2
  x = x + i;//3
  x += i;//4
 }

乍一看都没有问题,但是如果把这段程序编译,或者放在zi动编译的IDE中,第三行就会立刻报错:Type mismatch: cannot convert from int to short.

原因:

很多人认为简单赋值表达式x = x + i和复合赋值表达式x += i是完全等价的,复合赋值表达式只不过是简单赋值表达式的简写形式,但是通过上面的代码,我们看到在简单表达式编译出错的情况下,符合赋值表达式没有错误,因此这二者不是简单的完全等价。

java语言规范中定义复合赋值表达式为:E1 op = E2等价于简单赋值表达式:E1 = (T)((E1) op (E2)),其中T是E1的类型,因此符合赋值表达式会zi动地将它们所执行的ji算结果转型为其左侧变量的类型,如果结果的类型与该变量类型相同,则该类型转换不会造成任何影响,如果结果类型比该变量的类型要宽,则复合赋值表达式会zi动进行一个窄化原始类型的类型转换操作。

可以使用下面的程序来验证类型转换:

public static void main(String[] args) {
  short x = 0;
  int i = 123456;
  System.out.println(x += i);
 }
public static void main(String[] args) {
  short x = 0;
  int i = 123456;
  System.out.println(x += i);
 }

输出结果为:-7616。

相对应复合赋值表达式的类型转换,简单表达式x = x + i要将一个int类型的数值赋值给一个short类型的变量,由于类型不匹配,如果不进行显式的强制类型转换,编译器就会报类型不匹配错误,因此不能通过编译。

结论:

虽然简单赋值表达式不能通过编译,但是编译错误也告诉了程序员问题所在,复合赋值表达式虽然可以正常运行,但是在上面程序中有丢失了ji算精度,造成了令人意想不到的运算结果。因此在使用复合赋值表达式时,一定要注意左侧变量的数据类型大于等于ji算结果数据类型,避免类型窄化丢失精度的问题。

9.简单赋值表达式不等价于复合赋值表达式:

下面的程序哪一行有错误:

public static void main(String[] args) {
  Object x = "Buy";//1
  String i = "Effective java";//2
  x = x + i;//3
  x += i;//4
 }
public static void main(String[] args) {
  Object x = "Buy";//1
  String i = "Effective java";//2
  x = x + i;//3
  x += i;//4
 }

这个问题和第8个问题类似,只是这次是相反,复合赋值表达式出错,而简单赋值表达式正确。

没错,第4行会报错:The operator += is undefined for the argument type(s) Object, String。

原因:

java中复合赋值表达式的限制条件为:左右两个操作数是原始类型,原始类型的包装类型,对于对象类型的例外是:如果左侧变量是String类型,则右侧操作数可以是任意类型,表达式将执行字符串连接操作,但是不允许左侧是非原始类型包装类型和字符串的其他对象类型数据。

结论:

不能简单认为复合赋值表达式和简单赋值表达式是完全等价的,如果真的想执行字符串和对象类型的复合赋值表达式,请将字符串类型放在左边。