Puzzle1:
第一个迷惑性的错误,属于想当然的类型,不过,我好像不太会遇到,因为自己肯定会判断余数为0而不是1的(虽然并没有考虑到负数),另外,位操作才是王道!
/**
* 判断一个数的奇偶性,使用? % 2 == 1正确吗?
*/
public class P1 {
public static void main(String[] args) {
System.out.println("i isOddByOne isOddByZero isOddByBit");
for(int i = -5; i < 6; i++){
System.out.print(i + " " + isOddByOne(i));
System.out.print(" " + isOddByZero(i));
System.out.println(" " + isOddByBit(i));
}
}
public static boolean isOddByOne(int i){
return i % 2 == 1; // i < 0,i % 2 == -1, return false
}
public static boolean isOddByZero(int i){
return i % 2 != 0; // 优先使用第三个判断方法~
}
public static boolean isOddByBit(int i){
return (i & 1) != 0; // 对于1而言,其余位都为0,只有末位为1。与操作,仅当i的末位也为1时判断结果才能为true,也就是i必须为奇数。数学渣表示这里还不是一眼就看穿的,心好累。
}
}
对于负数,判断尾数为1是无效的,因为尾数为-1。
Puzzle2:
关于这个容易踩到的陷阱,从我第一天学习编程语言(没有错,就是C)的时候,就被老师重点指出过,也被各种书籍无数次提醒过,但日常其实还是一直用着float和double这种,一来是对精度要求有限,二来是很少意识到要避免这个问题,还没有被坑过,所以不深刻啊。。。
import java.math.BigDecimal;
/**
* 日常操作,2.00元扣除1.10元,余额是?
*/
public class P2 {
public static void main(String[] args) {
System.out.println(2.00 - 1.10); // 由于1.10并不能被精确地表示为一个double,导致结果不可能是0.90这个准确值,而是0.8999999999999999
System.out.println(new BigDecimal(1.10).toString()); // 尽量避免使用new BigDecimal(double),该构造器,会用参数的精确值来创建一个实例,而事实是,类似1.10这种,是没有所谓的正确精确值的,只能表示为1.100000000000000088817841970012523233890533447265625,并不是想要的1.10。
System.out.println(new BigDecimal("2.00").subtract(new BigDecimal("1.10"))); // : 0.90,使用BidDecimal,会比使用原生类型来的更慢,如果确实在意,可以考虑改变数值的单位,如“元”改为“分”,这样的话,2.00和1.10就能当做200和110两个整数来处理,即精确又迅速。
}
}
Puzzle3:
/**
* 长整除,不是最终类型为long就一定不溢出
*/
public class P3 {
public static void main(String[] args) {
final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000; // 微秒 int类型的范围是-2147483648 ——2147483647,尽管该变量的类型是long,但直到结束所有乘法运算之前,都还是int类型,于是溢出,导致结果不对。在平时的编程中,一定要注意这种静默出现的错误,没摔过跟头之前,总觉得难以记牢。。。
final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000; // 毫秒
System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY); // : 5
final long MICROS_PER_DAYL = 24L * 60 * 60 * 1000 * 1000;
final long MILLIS_PER_DAYL = 24L * 60 * 60 * 1000;
System.out.println(MICROS_PER_DAYL / MILLIS_PER_DAYL); // : 1000
}
}
Puzzle4:
/**
* 论长整数时L而非l的重要性
*/
public class P4 {
public static void main(String[] args) {
// Source Code Pro还是可以清楚看出1和l的区别的,压力不大O(∩_∩)O哈哈~
System.out.println(12345 + 5432l); // : 17777
}
}
Puzzle5:
/**
* 十六进制,避免混合类型的计算
*/
public class P5 {
public static void main(String[] args) {
System.out.println(Long.toHexString(0x100000000L + 0xcafebabe)); // : cafebage
// 对于八进制或者十六进制,最高位表示正负号,0xcafebabe是int型,故最高位是1,表示负数,等于-889275714,也等于0xffffffffcafebageL,也因此运行结果丢失了开头的1。
System.out.println(Long.toHexString(0x100000000L + 0xcafebabeL)); // : 1cafebage # 尽量避免混合类型的计算,都设置为长整型,就不会有这种问题了!
}
}
Puzzle6:
不得不说,原码、反码、补码这块知识点,头疼了好久,底层的东西,感觉不容易理解,又容易忘却。
/**
* 多重转型
* int 32位 char 16位(无符号) byte 8位
* Java使用基于2的补码的二进制运算!!!
* 正数的补码等于原码,负数的补码为原码取反(符号位不变,即反码)再加一
*/
public class P6 {
public static void main(String[] args) {
// System.out.println(Integer.toBinaryString(-1)); // : 11111111111111111111111111111111 # 32个1
System.out.println((int)(char)(byte)-1); // : 65535 #
// (byte)-1 == (原码)10000001 == (补码)11111111
// (char)(byte)-1 == (补码)1111111111111111 // 由byte转char,因为byte是有符号的,所以执行符号扩展
// (int)(char)(byte)-1 == (补码)00000000000000001111111111111111 == (原码)00000000000000001111111111111111 == 65535 // 由char转int,由于char是无符号位的,因此执行零扩展,直接导致-1变成了65535
// 不得不说,当初上学那会,对原码、反码、补码纠结着,总记着那个时钟的例子。。。
}
}
我就不说这种错误我是不会犯的,因为我就没想过用异或这种东西,我好菜。。。
/**
* 通过异或运算符互换内容,在Java里可不要想着一行完成这种事,得不偿失
*/
public class P7 {
public static void main(String[] args) {
int x = 1984; // 0x7c0
int y = 2001; // 0x7d1
x ^= y ^= x ^= y; // 据说在C或者C++中,能实现互换内容,我已经不写C好多年,谁知道呢→_→
System.out.println("x = " + x + "; y = " + y); // : x = 0; y = 1984 #
/* 假设按照C的处理方式,来计算下:
x == 1984 == 0x7c0 == (原码)0000 0111 1100 0000 == (反码)0000 0111 1100 0000 == (补码)0000 0111 1100 0000
y == 2001 == 0x7d1 == (原码)0000 0111 1101 0001 == (反码)0000 0111 1101 0001 == (补码)0000 0111 1101 0001
x == x^y == 0000 0000 0001 0001
y == y^x == 0000 0111 1100 0000
x == x^y == 0000 0111 1101 0001
由此可见,确实是互换了内容,但是!!!!!!这是假设按照C的处理方式,事实上,Java语言规范描述了“操作符的操作数是从左向右求值的,为了求x^=expr的值,在计算expr之前提取x的值”
The Java language specification says that operands of operators are evaluated from left to right [JLS 15.7]. To evaluate the expression x ^= expr, the value of x is sampled before expr is evaluated, and the exclusive OR of these two values is assigned to the variable x [JLS 15.26.2].
显然,按照Java的做法,这段代码是不可能实现互换内容的,当然,非要实现,可以改为y = (x^= (y^= x)^ y,但是,这种代码,和以前在C语言书上看过的i = ++i++++之流有何区别,可读性差,容易出错,好处就仅仅是节省了一行空间?!完全是bad smell
*/
}
}
又是混合类型造成的,你让我这个从没踩过类似坑的是该笑呢还是该哭,我太菜了是吧╮(╯▽╰)╭
/**
* 三目运算符的陷阱
*/
public class P8 {
public static void main(String[] args) {
char x = 'X';
int i = 0;
final int LX = 89;
System.out.println(true ? x : 0); // : X # 0为常数,返回值类型为char
System.out.println(false ? i : x); // : 88 # i为变量,二进制数字提升
System.out.println(true ? x : i); // : 88 # i为变量,二进制数字提升
System.out.println(true ? x : LX); // : X # LX为常量,返回值类型为char
System.out.println(false ? x : LX); // : Y # LX为常量,返回值类型为char
/*
对于三目运算符,如果第二个和第三个参数类型相同,则返回值类型也一致,否则,大致按照以下规则:
如果一个类型为T(byte short char),另一个为int类型的常量表达式(注意,这里第二个参数和第三个参数,是平等地位,别理解成了以第二个参数为主),则返回值类型为T,否则,对两个参数类型进行二进制数字提升,提升之后的类型即为返回值类型。
总而言之,不要使用混合类型!!!
*/
}
}
原来复合赋值操作符这么危险,哎,再感慨一句,我又没遇到过,菜!
/**
* 复合赋值操作符的陷阱 k op v 等价于 k = (K)(k op v) K为k的类型
*/
public class P9 {
public static void main(String[] args) {
short x = 1;
int i = 123456;
x += i; // 这里会将 x + i的结果转换为short类型,如果值过大,就会出现不正确的结果了
System.out.println(x); // : -7615 # 123457对于short来说太大了,自动转换时得到不正确的结果
x = (short) (x + i); // 将int隐式转为short是非法的,可以显式转换,但是,你还要确保结果不出错呦
}
}
Puzzle10:
复合赋值操作符除了上一篇讲的默认转类型外,还有另一个要求!
/**
* 复合赋值操作符( += -= *= /= %= <<= >>= >>>= &= ^= != )的陷阱
*/
public class P10 {
public static void main(String[] args) {
Object x = "L";
String i = "X";
x = x + i; // String可以向上转型为Object类型
i += x; // 左侧为String类型,允许右侧为任何类型
x += i; // 左侧为Object类型,不可以,此行报错,无法运行!
/*
复合赋值操作符要求两个操作数都是原生类型,例如int,或者包装了的原生类型,例如Integer,唯一的例外是当左侧的操作数为String类型时,右侧可以为任何类型,在这种情况下,执行的是字符串连接操作。
*/
}
}