Java Puzzlers(一 表达式计算)
1 奇数判断
误:public static boolean isOdd(int i) { return i % 2 == 1; } //没有考虑到负奇数的情况
正:return i % 2 != 0; 更好的性能:return (i & 1) != 0;
总结:求余操作需要考虑符号!
2 浮点数计算
public static void main(String args[]) { System.out.println(2.00 - 1.10); } //天真以为得到0.90
如果熟悉 Double.toString 的文档,估计会觉得 double 会转为string,程序会打印出足够区分 double值的小数部分,小数点前或后面至少一位。这样说来应该是0.9,可惜运行程序发现是 0.8999999999999999。问题是数字1.1不能被double准确表示!只能用最接近的double值表示。遗憾的是结果不是最接近0.9的double值。更普遍的看这问题是: 不是所有的十进制数都能用二进制浮点数准确的表示 。如果用jdk5或以后版本,你可能会使用 printf来准确设置:
// Poor solution - still uses binary floating-point!
System.out.printf("%.2f%n", 2.00 - 1.10);
现在打印出来是正确的了,但治标不治本:它仍然使用的是 double运算(二进制浮点),浮点计算在大范围内提供近似计算,但不总是产生准确的结果。二进制浮点数特别不适合金融计算,因为他不可能表示0.1——或任何10的负幂——exactly as a finite-length binary fraction。
一种解决办法是使用基本类型,比如int long,然后扩大操作数倍数做计算。如果用这种流程,确保基本类型足够大来表示你所有你用到的数据,这个例子中,int足够了System.out.println((200 - 110) + " cents");
另一种办法使用 BigDecimal,他进行准确的十进制计算,他还能通过JDBC和SQL的 DECIMAL类型合作。 有一个箴言:总是使用 BigDecimal(String)构造器,绝不使用 BigDecimal(double).后面这个构造函数用参数的准确值创建一个实例: new BigDecimal(.1)返回一个 BigDecimal表示0.1000000000000000055511151231257827021181583404541015625。正确使用会得到预期结果 0.90: System.out.println(new BigDecimal("2.00"). subtract(new BigDecimal("1.10"))); 这个例子不是特别漂亮,java没有给 BigDecimal提供语言学上的支持, BigDecimal也可能比使用基本类型(对大量使用十进制计算的程序比较有用)更慢,大多数情况没有这个需要。
总结:但需要准确答案的时候,避免使用 float and double;金融计算,使用 int, long, or BigDecimal。对语言设计者来说,提供十进制计算的语言支持。一个方法是给操作符重载提供有限的支持,
这样计算操作符就能和数字引用类型比如BigDecimal一起工作?
。另一种方法就是像COBOL and PL/I一样,提供基本十进制类型。
3 长整型除法
被除数表示一天的微秒数,除数表示一天的毫秒数:
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!问题出在计算 MICROS_PER_DAY 时溢出了,虽然结果是满足long的,但不满足int。这个计算过程全部是按int 计算的,计算完之后才转为long。因此很明显计算过程中溢出。为什么会用int计算?因为因子都是int型的,Java没有 target typing特性(就是根据结果的类型来确定计算过程所用类型)。解决这个问题很简单,把第一个因子设置为long,这样会强制所有以后的计算都用long进行。虽然很多地方都不需要这么做,但这是一个好习惯。
final long MICROS_PER_DAY = 24 L * 60 * 60 * 1000 * 1000;
final long MILLIS_PER_DAY = 24 L * 60 * 60 * 1000;
我们得到一个教训:和大数据打交道的时候,小心溢出!一个变量能装得下结果,并不代表计算过程中会确保得到正确类型。
4 小学生都知道的事情
System.out.println(12345 + 5432l); // 毫无疑问的66666? 看仔细了!输出17777
教训:使用long的时候用大写的L,绝不用小写的l,类似的避免用l作为变量名。很难看出输出的是1还是l
// Bad code - uses el (l) as a variable name
List<String> l = new ArrayList<String>();
l.add("Foo");
System.out.println(1);
5 十六进制的快乐
System.out.println(Long.toHexString(0x100000000L + 0xcafebabe)); //输出cafebabe,最左边的1丢了!
十进制有一个十六或八进制都没有的优点:数字都是正的,想表达负数需要一个负号。这样的话写十进制的int或long,不管正负都很方便。十六或八进制就不这样了,必须由高位来决定正负。这个例子中,0xcafebabe 是一个int常量,最高位是1,因此是负数=十进制 -889275714。这里还有一个混合类型计算的额外操作:左边操作数是long,右边是int,计算时Java通过widening primitive conversion 把int变为long,再加这两个long。因为int是有符号整型,转变执行了一个符号扩展:把负的int值提升为数值相等的long值。右边的0xcafebabe被提升为long值 0xffffffffcafebabeL,再加到左边0x100000000L上。当被看作int型的时候,0xcafebabe扩展出来的高32位是-1,而左边操作数高32位是1,相加之后为0,这解释了为什么最高位的1丢失。解决方法是把右边的操作数也写上long,这样就避免了符号扩展的破坏。
System.out.println(Long.toHexString(0x100000000L + 0xcafebabeL));
教训:考虑十六或八进制自身带正负,混合类型计算让人迷惑。为避免出错,最好不要使用混合类型计算。对语言设计者来说,考虑支持无符号整数类型来去掉符号扩展的可能。有人争论十六或八进制负数应该被禁止,但对于程序员来说非常不好,他们经常使用十六进制来表示符号没有意义的数值。
6 多重映射
System.out.println((int) (char) (byte) -1);
以int 类型的-1开始,映射到byte,到char,最后返回int。第一次32位变到8位,到16位,最后回到32位。最后发现值并没有回到原始!输出65535
问题来自映射时的符号扩展问题。int值-1的所有32位都是1,转为8位byte很直观,只留下低八位就行,仍然是-1.转char的时候,就要小心了,byte是有符号的,char无符号。通常有可能保留数值的同时把一个整型转到更“宽”的类型,但不可能用char来表示一个负的byte值。Therefore, the conversion from byte to char is not considered a widening primitive conversion [JLS 5.1.2], but a widening and narrowing primitive conversion [JLS 5.1.4]: The byte is converted to an int and the int to a char。看起来较复杂,但有一个简单规则描述窄变宽转换时的符号扩展:原始值有符号就做符号扩展;不管转换成什么类型,char只做零扩展。因为byte是有符号的,byte -1转成char会有符号扩展。结果是全1的16位,也就是 216 – 1或65,535。char到int宽扩展,规则告诉我们做零扩展。int类型的结果是65535。虽然规则简单,但最好不要写依赖这规则的程序。如果你是宽转换到char,或从char转换(char总是无符号整数),最好显式说明。
如果从char类型的c宽转换,并且不想符号扩展,虽然不需要,但为了清晰可以这样:
int i = c & 0xffff;
还可以写注释:
int i = c; // Sign extension is not performed
如果从char类型的c宽转换,并且想符号扩展,强制char到short(宽度一样但有符号)
int i = (short) c; // Cast causes sign extension
byte到char,不要符号,必须用位屏蔽抑制他,这是惯例不用注释( 0xff这种0x开头的默认是int类型的?)
char c = (char) (b & 0xff);
byte to a char ,要符号,写注释
char c = (char) b; // Sign extension is performed
这一课很简单: 如果你不能清晰看出程序在干什么,他可能就没有按你希望的在运行。拼命寻求清晰,虽然整数转换的符号扩展规则简单,但大多程序员不知道。如果你的程序依赖于他,让你的意图明显。
7 交换美味
在一个简单表达式中,不要对一个变量赋值超过一次。更普遍的说,不要用“聪明”的程序技巧。
8 Dos Equis
char x = 'X';
int i = 0;
System.out.print(true ? x : 0);
System.out.print(false ? i : x);
输出XX?可惜输出的是X88。注意第二三个操作数类型不一样,第5点说过,混合类型计算让人迷惑!条件表达式中是最明显的地方。虽然觉得两个表达式结果应该相同,毕竟他们类型相似,只是位置相反而已,但结果并不是这样。
决定条件表达式结果类型的规则很多,但有三个关键点:
1 如果第二三个操作数类型一样,表达式也是这个类型,这样就避免了混合类型计算。
2 3 复杂略过 总之第一个表达式是调用了PrintStream.print(char),第二个是PrintStream.print(int) 造成结果不同
总结:最好在条件表达式中第二三个操作数用同一种类型
9
x += i; // 等同于x = x + i;?
compound assignment expressions automatically cast the result of the computation they perform to the type of the variable on their left-hand side// 暗含映射
例如 short x = 0;int i = 123456;
x += i; // –7,616,int值123456太大,short装不下,高位两个字节被去掉
x = x + i; // 编译错误- "possible loss of precision"
为避免危险,不要在byte, short, or char上面用复合赋值符。当在int上用时,确保右边不是long, float, or double类型。在float上用,确保右边不是double。
10 复合赋值符需要两边操作数都为基本类型或boxed primitives,如int ,Integer。有一例外:+= 左边为String的话,允许右边为任意类型。这时做的是字符串拼接操作。
Object x = "Buy ";
String i = "Effective Java!";
x = x + i; //x+i 为String,和Object兼容,因此表达式正确
x += i; //非法左边不是String
注意返回类型:
误:public static boolean isOdd(int i) { return i % 2 == 1; } //没有考虑到负奇数的情况
正:return i % 2 != 0; 更好的性能:return (i & 1) != 0;
总结:求余操作需要考虑符号!
2 浮点数计算
public static void main(String args[]) { System.out.println(2.00 - 1.10); } //天真以为得到0.90
如果熟悉 Double.toString 的文档,估计会觉得 double 会转为string,程序会打印出足够区分 double值的小数部分,小数点前或后面至少一位。这样说来应该是0.9,可惜运行程序发现是 0.8999999999999999。问题是数字1.1不能被double准确表示!只能用最接近的double值表示。遗憾的是结果不是最接近0.9的double值。更普遍的看这问题是: 不是所有的十进制数都能用二进制浮点数准确的表示 。如果用jdk5或以后版本,你可能会使用 printf来准确设置:
// Poor solution - still uses binary floating-point!
System.out.printf("%.2f%n", 2.00 - 1.10);
现在打印出来是正确的了,但治标不治本:它仍然使用的是 double运算(二进制浮点),浮点计算在大范围内提供近似计算,但不总是产生准确的结果。二进制浮点数特别不适合金融计算,因为他不可能表示0.1——或任何10的负幂——exactly as a finite-length binary fraction。
一种解决办法是使用基本类型,比如int long,然后扩大操作数倍数做计算。如果用这种流程,确保基本类型足够大来表示你所有你用到的数据,这个例子中,int足够了System.out.println((200 - 110) + " cents");
另一种办法使用 BigDecimal,他进行准确的十进制计算,他还能通过JDBC和SQL的 DECIMAL类型合作。 有一个箴言:总是使用 BigDecimal(String)构造器,绝不使用 BigDecimal(double).后面这个构造函数用参数的准确值创建一个实例: new BigDecimal(.1)返回一个 BigDecimal表示0.1000000000000000055511151231257827021181583404541015625。正确使用会得到预期结果 0.90: System.out.println(new BigDecimal("2.00"). subtract(new BigDecimal("1.10"))); 这个例子不是特别漂亮,java没有给 BigDecimal提供语言学上的支持, BigDecimal也可能比使用基本类型(对大量使用十进制计算的程序比较有用)更慢,大多数情况没有这个需要。
总结:但需要准确答案的时候,避免使用 float and double;金融计算,使用 int, long, or BigDecimal。对语言设计者来说,提供十进制计算的语言支持。一个方法是给操作符重载提供有限的支持,
3 长整型除法
被除数表示一天的微秒数,除数表示一天的毫秒数:
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!问题出在计算 MICROS_PER_DAY 时溢出了,虽然结果是满足long的,但不满足int。这个计算过程全部是按int 计算的,计算完之后才转为long。因此很明显计算过程中溢出。为什么会用int计算?因为因子都是int型的,Java没有 target typing特性(就是根据结果的类型来确定计算过程所用类型)。解决这个问题很简单,把第一个因子设置为long,这样会强制所有以后的计算都用long进行。虽然很多地方都不需要这么做,但这是一个好习惯。
final long MICROS_PER_DAY = 24 L * 60 * 60 * 1000 * 1000;
final long MILLIS_PER_DAY = 24 L * 60 * 60 * 1000;
我们得到一个教训:和大数据打交道的时候,小心溢出!一个变量能装得下结果,并不代表计算过程中会确保得到正确类型。
4 小学生都知道的事情
System.out.println(12345 + 5432l); // 毫无疑问的66666? 看仔细了!输出17777
教训:使用long的时候用大写的L,绝不用小写的l,类似的避免用l作为变量名。很难看出输出的是1还是l
// Bad code - uses el (l) as a variable name
List<String> l = new ArrayList<String>();
l.add("Foo");
System.out.println(1);
5 十六进制的快乐
System.out.println(Long.toHexString(0x100000000L + 0xcafebabe)); //输出cafebabe,最左边的1丢了!
十进制有一个十六或八进制都没有的优点:数字都是正的,想表达负数需要一个负号。这样的话写十进制的int或long,不管正负都很方便。十六或八进制就不这样了,必须由高位来决定正负。这个例子中,0xcafebabe 是一个int常量,最高位是1,因此是负数=十进制 -889275714。这里还有一个混合类型计算的额外操作:左边操作数是long,右边是int,计算时Java通过widening primitive conversion 把int变为long,再加这两个long。因为int是有符号整型,转变执行了一个符号扩展:把负的int值提升为数值相等的long值。右边的0xcafebabe被提升为long值 0xffffffffcafebabeL,再加到左边0x100000000L上。当被看作int型的时候,0xcafebabe扩展出来的高32位是-1,而左边操作数高32位是1,相加之后为0,这解释了为什么最高位的1丢失。解决方法是把右边的操作数也写上long,这样就避免了符号扩展的破坏。
System.out.println(Long.toHexString(0x100000000L + 0xcafebabeL));
教训:考虑十六或八进制自身带正负,混合类型计算让人迷惑。为避免出错,最好不要使用混合类型计算。对语言设计者来说,考虑支持无符号整数类型来去掉符号扩展的可能。有人争论十六或八进制负数应该被禁止,但对于程序员来说非常不好,他们经常使用十六进制来表示符号没有意义的数值。
6 多重映射
System.out.println((int) (char) (byte) -1);
以int 类型的-1开始,映射到byte,到char,最后返回int。第一次32位变到8位,到16位,最后回到32位。最后发现值并没有回到原始!输出65535
问题来自映射时的符号扩展问题。int值-1的所有32位都是1,转为8位byte很直观,只留下低八位就行,仍然是-1.转char的时候,就要小心了,byte是有符号的,char无符号。通常有可能保留数值的同时把一个整型转到更“宽”的类型,但不可能用char来表示一个负的byte值。Therefore, the conversion from byte to char is not considered a widening primitive conversion [JLS 5.1.2], but a widening and narrowing primitive conversion [JLS 5.1.4]: The byte is converted to an int and the int to a char。看起来较复杂,但有一个简单规则描述窄变宽转换时的符号扩展:原始值有符号就做符号扩展;不管转换成什么类型,char只做零扩展。因为byte是有符号的,byte -1转成char会有符号扩展。结果是全1的16位,也就是 216 – 1或65,535。char到int宽扩展,规则告诉我们做零扩展。int类型的结果是65535。虽然规则简单,但最好不要写依赖这规则的程序。如果你是宽转换到char,或从char转换(char总是无符号整数),最好显式说明。
如果从char类型的c宽转换,并且不想符号扩展,虽然不需要,但为了清晰可以这样:
int i = c & 0xffff;
还可以写注释:
int i = c; // Sign extension is not performed
如果从char类型的c宽转换,并且想符号扩展,强制char到short(宽度一样但有符号)
int i = (short) c; // Cast causes sign extension
byte到char,不要符号,必须用位屏蔽抑制他,这是惯例不用注释( 0xff这种0x开头的默认是int类型的?)
char c = (char) (b & 0xff);
byte to a char ,要符号,写注释
char c = (char) b; // Sign extension is performed
这一课很简单: 如果你不能清晰看出程序在干什么,他可能就没有按你希望的在运行。拼命寻求清晰,虽然整数转换的符号扩展规则简单,但大多程序员不知道。如果你的程序依赖于他,让你的意图明显。
7 交换美味
在一个简单表达式中,不要对一个变量赋值超过一次。更普遍的说,不要用“聪明”的程序技巧。
8 Dos Equis
char x = 'X';
int i = 0;
System.out.print(true ? x : 0);
System.out.print(false ? i : x);
输出XX?可惜输出的是X88。注意第二三个操作数类型不一样,第5点说过,混合类型计算让人迷惑!条件表达式中是最明显的地方。虽然觉得两个表达式结果应该相同,毕竟他们类型相似,只是位置相反而已,但结果并不是这样。
决定条件表达式结果类型的规则很多,但有三个关键点:
1 如果第二三个操作数类型一样,表达式也是这个类型,这样就避免了混合类型计算。
2 3 复杂略过 总之第一个表达式是调用了PrintStream.print(char),第二个是PrintStream.print(int) 造成结果不同
总结:最好在条件表达式中第二三个操作数用同一种类型
9
x += i; // 等同于x = x + i;?
compound assignment expressions automatically cast the result of the computation they perform to the type of the variable on their left-hand side// 暗含映射
例如 short x = 0;int i = 123456;
x += i; // –7,616,int值123456太大,short装不下,高位两个字节被去掉
x = x + i; // 编译错误- "possible loss of precision"
为避免危险,不要在byte, short, or char上面用复合赋值符。当在int上用时,确保右边不是long, float, or double类型。在float上用,确保右边不是double。
10 复合赋值符需要两边操作数都为基本类型或boxed primitives,如int ,Integer。有一例外:+= 左边为String的话,允许右边为任意类型。这时做的是字符串拼接操作。
Object x = "Buy ";
String i = "Effective Java!";
x = x + i; //x+i 为String,和Object兼容,因此表达式正确
x += i; //非法左边不是String
注意返回类型:
The arithmetic, increment and decrement,
bitwise, and shift operators return a double if at least one of the operands is a double. Otherwise, they return a float if at least one of the operands is a float. Otherwise, they return a long if at least one of the operands is a long. Otherwise, they return an int, even if both operands are byte, short, or char types that are narrower than int.