1、奇数性
下面的方法是否能确定其参数是否为奇数:
public static boolean isOdd(int i) {
return i % 2 == 1;
}
奇数可定义为被2整除余数为1的整数,因此上面的方法看起来可行。但是很遗憾,在四分之一的时间里它返回的都是错误答案。
为什么是四分之一呢?因为在所有int数值中,有一半是负数,而isOdd方法对所有的付奇数的判断都会失败。
其实,这是Java对取余操作符%的定义产生的结果。该操作符被定义为对所有的int数值a和非零int数值b,都满足下面的恒等式:
(a / b) * b + (a % b) == a
也就是,用b整除a,将商乘以b,然后加上余数,就得到最初的值a了。但是当与Java的结尾整数整除操作符结合时,它就意味着:当取余操作返回一个非零的结果时,它与左操作数具有相同的正负号。
所以,当i时负奇数时,i % 2等于-1而不是1。
正确的操作有两种:
// 第一种方法:使用相反的比较含义,只需将i % 2与0而不是1比较即可
public static boolean isOdd1(int i) {
return i % 2 != 0;
}
// 第二种方法:使用位操作符&替代取余操作符,逻辑操作也快于取余操作
public static boolean isOdd2(int i) {
return (i & 1) != 0;
}
2、浮点数的精确性
// 以下代码会打印什么呢?会是0.9吗?
public static void main(String[] args) {
System.out.println(2.00 - 1.10);
}
很不靠谱,该程序打印的是0.8999999999999999。问题在于1.1这个数字不能被精确表示为一个double,而是一个最接近它的double值。所以不止Java中在一些其他语言中也是一样,并不是所有的小数都可以用二进制浮点数精确表示。因此,二进制浮点对于货币计算是非常不合适的,原因在于它不可能将0.1或者10的其他任何次负幂,精确表示为一个有限长度的二级制小数。
解决问题的一种方式是使用某种整数类型,比如int或long,并用分为单位来计算。
另一种解决方式是使用Java为执行精确小数计算设计的BigDecimal类。它还可以通过JDBC与SQL DECIMAL类型进行互操作。注意:一定要用BigDecimal(String) 构造器,千万不要用BigDecimal(double)。后一种构造器将用它的参数的精确值来创建一个实例。例如new Decimal(0.1), 它将返回一个BigDecimal,它存储的浮点数为0.1000000000000000055511151231257827021181583404541015625
总之,在需要进行精确计算的地方,避免使用float或double类型,尤其是货币计算。
3、长整除
// 以下方法会打印什么?
public static void LongDivision() {
final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;
final long MILLS_PER_DAY = 24 * 60 * 60 * 1000;
System.out.println(MICROS_PER_DAY / MILLS_PER_DAY)
}
也许,很多人会说打印1000。答案似乎很明显,但是事实是这样吗?遗憾的是,它打印的是5。怎么回事呢?问题在于常数MICROS_PER_DAY的计算溢出了。虽然计算结果可以放入long中,但是并不适合放入int中。此计算的所有因子都是int类型,所以是以int运算执行的,只有在运算完成后,才将结果提升为long。但是计算已经溢出。
当两个int数值相乘时,将得到一个int数值。Java不具有目标确定类型的特性,其含义是指存储结果的变量类型会影响计算所使用的类型。
通过使用long常量来替代每一个乘积的第一个因子,可以改成这个程序。这样做可以强制表达式中所有的后续计算都用long运算来完成。
// 正确的程序代码
public static void LongDivision() {
final long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
final long MILLS_PER_DAY = 24L * 60 * 60 * 1000;
System.out.println(MICROS_PER_DAY / MILLS_PER_DAY)
}
4、十六进制的趣事
//程序会打印什么?
public static void joyOfHex() {
System.out.println(Long.toHexString(0x100000000L + 0xcafebabe));
}
看似很明显,应该打印1cafebabe。但是,实际打印的是cafebabe,并没有前导的1。什么原因呢?因为十六进制和八进制字面常量不能确定它的正负。如果十六进制和八进制字面常量的最高位被置位了,那么它们就是负数。在此程序中,0xcafebabe是int常量,它的最高位被置位了,所以它是一个负数。
该程序执行的是一种混合类型的计算:左操作数是long类型,右操作数是int类型。为执行该计算,Java将int类型的数值用拓宽原生类型转换提升为long类型,然后对两个long类型数值相加。而int是有符号的,所以进行符号拓展,将负的int类型数值提升为一个数值上相等的long类型数值。0xcafebabe被提升为long类型的数值0xffffffffcafebabeL。既:
0xffffffffcafebabeL
+ 0x0000000100000000L
————————————————————————
0x00000000cafebabeL
如果希望打印期望的1cafebabe结果,只需要将0xcafebabe改为long类型的0xcafebabeL,即可避免符号拓展的破坏。通常最后避免混合类型计算。
5、多重转型
转型用于将一个数值从一中类型转换到另一种类型。
// 以下程序会打印什么?
public static multicast() {
System.out.println((int)(char)(byte)-1);
}
它会最终回到起点的-1吗?执行程序,它会打印65535。是不是很迷惑?
该程序的行为与Java的转型的符号拓展行为有关。Java使用基于2的补码的二进制运算,所以int类型的-1的所有32位都是置位的(其他位为1)。从int到byte的转型执行了一个窄化原生类型转换,直接将除低8位之外的所以位砍掉。留下了一个8为的byte,它仍旧是-1。
从byte到char的转型比较麻烦,因为byte是有符号类型,而char是无符号类型。在将一个整数类型转换成另一个宽度更宽的整数类型时,通常可以保持数值不变,但是不能用一个char表示一个负的byte数值。所以byte到char的转换不是一个拓宽原生类型转换,而是一个拓宽并窄化原生类型的转换,既byte转换成int,int又被转换成char。有一条简单的规则能够描述从较窄的整型转换成较宽的整型时的符号拓展行为:如果最初的数值类型是有符号的,那么就执行符号拓展;如果是char,那么不管将被转换成什么类型,都执行零拓展。所有byte是有符号类型,在将byte数值转为char时会发生符号拓展。结果的char数值的16位都被置位了,数值变为65535。从char到int是一个拓宽原生类型转换,将执行零拓展,所有结果还是65535。
// 将一个char转换为一个更宽的类型,且不希望有符号拓展,为清晰表达意图,可以使用一个位掩码
// c为一个char类型的变量
int i = c & 0xffff;
// 将一个byte数值转换为char,且不希望有符号拓展,必须使用一个位掩码限制。这是一个通用的做法
// b为一个byte类型的变量
char c = (char) (b & 0xff)
如果通过观察不能确定程序将做什么,那么它做的很可能就不是你想要的。要明白清晰地表达你的意图。
6、三元运算
// 以下两个打印语句分别会打印什么?
public static void cleverSwap() {
char x = 'X';
int i = 0;
System.out.print(true ? x : 0);
System.out.print(false ? i : x);
}
会是XX吗?运行程序,你会发现打印的是X88。答案就在条件表达式规范的一个阴暗角落里。这两个表达式中,每个表达式的第二个和第三个操作数的类型都是不同的。记住:混合类型的计算会引起混乱。这一点在条件表达式中比在其他地方都表现得更明显。
确定条件表达式结果类型的规则如下
1、如果第二个和第三个操作数具有相同的类型,那它就是条件表达式的类型
2、如果一个操作数的类型为T,T表示byte、short或char,另一个操作数是一个int类型的常量表达式,那它的值可以用类型T表示,条件表达式的类型就是T。
3、否则,将对操作数类型进行二进制数字提升,而条件表达式的类型就是第二个和第三个操作数被提升后的类型。
其中第二条用于第一个表达式,第三条用于第二个表达式。
7、+操作
// 给出对变量x和i的声明,使得第一个表达式合法、第二个表达式不合法和第一个表达式不合法、第二个表达式合法。
x += i;
x = x + i;
许多程序员认为第一个表达式只是第二个表达式的简写。但是Java语言规范讲到,符合赋值E1 op = E2等价于E1 = (T)((E1) op (E2)),其中T是E1的类型,除非E1只被计算一次。换句话说,复合赋值表达式自动地将所执行计算的结果转型为其左侧变量的类型。
short x = 0;
int i = 123456;
// 合法语法,包含一个隐藏的类型强转,可能会丢失精度。
x += i; // 等价于x = (short) (x + i)
// 非法语法
x = x + i; // 将int类型赋值给short类型,需要显示转型
Object x = "Buy";
String i = "Effective Java!";
// 非法语法,String无法转型为Object
x += i; // 等价于 x = (Object)(x + i)
// 合法语法,String可以兼容Object
x = x + i;