Java Puzzlers(三-二 循环)

Java Puzzlers(三-二 循环)

31
while (i != 0)
    i >>>= 1; //无符号右移,不管正负左边都是补0
为了表达式合法,这里的i必须是整型(byte, char, short, int, or long)。谜题的关键在于>>>= 是一个复合赋值操作符,不幸的是复合赋值操作符会默默的做narrowing primitive conversions,即从一个数据类型转换为一个更小的数据类型。Narrowing primitive conversions can lose information about the magnitude or precision of numeric values。为了使问题更具体,假设这样定义:
short i = -1; 因为初始值i ((short)0xffff) 非零,循环执行。第一步位移会把i提升为int。short, byte, or char类型的操作数都会做这样的操作。这是widening primitive conversion,没有信息丢失。这种提升有符号扩展,因此结果是int值0xffffffff。无符号右移一位产生int值0x7fffffff。为了把int值存回short变量,Java执行了可怕的narrowing primitive conversion,即简单去掉高十六位。这样又变回了(short)0xffff。如果定义类似short or byte型的负数,都会得到类似结果。你如果定义的是char话,则不会无限循环,因为char值非负,位移之前的宽扩展不会做符号扩展。

总结:不要在short, byte, or char变量上使用复合赋值操作符。这种表达式进行混合类型计算,非常容易混淆。更糟糕的是隐含的窄映射会丢掉信息。

32
while (i <= j && j <= i && i != j) {}
i <= j and j <= i, surely i must equal j?对于实数来说是这样的。他非常重要,有个名称:The ≤ relation on the real numbers is said to be antisymmetric。Java's <= operator used to be antisymmetric before release 5.0, but no longer.Java 5之前数据比较符号(<, <=, >, and >=) 需要两边的操作数必须为基础类型(byte, char, short, int, long, float, or double).在Java 5变为两边操作数为凡是可转变为基础类型的类型。java 5引入autoboxing and auto-unboxing 。The boxed numeric types are Byte, Character, Short, Integer, Long, Float, and Double。具体点,让上面进入无限循环:
Integer i = new Integer(0);
Integer j = new Integer(0);
(i <= j and j <= i) perform unboxing conversions on i and j and compare the resulting int values numerically。i和j表示0,所以表达式为true。i != j比较的是对象引用,也为true。很奇怪规范没有把等号改为比较值。原因很简单:兼容性。当一种语言广泛应用的时候,不能破坏已存在的规范来改变程序的行为。System.out.println(new Integer(0) == new Integer(0));总是输出false,所以必须保留。当一个是boxed numeric 类型,另一个是基本类型的时候可以值比较。因为java 5之前这是非法的,具体点:
System.out.println(new Integer(0) == 0); //之前的版本非法,Java 5输出True
总结:当两边的操作数是boxed numeric类型的时候,数字比较符和等于符号是根本不同的:数字比较符是值比较,等号比较的是对象引用

33
while (i != 0 && i == -i) {}
有负号表示i一定是数字,NaN不行,因为他不等于任何数。事实上,没有实数可以出现这种情况。但Java的数字类型并没有完美表达实数。浮点数由一个符号位,一个有效数字(尾数),一个指数构成。浮点数只有0才会和自己的负数相等,所以i肯定是整数。有符号整数用的是二进制补码计算:取反加一。补码的一大优势是用一个唯一的数来表示0。然而有一个对应的缺点:本来可表达偶数个值,现在用一个表达了0,剩下奇数个来表示正负数,意味着正数和负数的数量不一样。比如int值,他的Integer.MIN_VALUE(-231)。十六进制表达0x8000000。通过补码计算可知他的负数仍然不变。对他取负数是溢出了的,不过Java在整数计算中忽略了溢出。
总结:Java使用二进制补码,是不对称的。有符号整数(int, long, byte, and short) 负数值比整数值多一个。

34
        final int START = 2000000000;
        int count = 0;
        for (float f = START; f < START + 50; f++)
            count++;
        System.out.println(count);
注意循环变量是float。回忆谜题28 ,明显f++没有任何作用。f的初始化值接近Integer.MAX_VALUE,因此需要31位来准确表达,但是float类型只提供了24位精度。增加这样大的一个float值不会改变值。看起来会死循环?运行程序会发现,输出0。循环中f和(float)(START + 50)做比较。但int和float比较的时候,自动把int先提示为float。不幸的是三个会引起精度丢失的宽基本类型转换的其中之一(另外两个是long到float,long到double)。f的初始值巨大,加50再转换为float和直接把f转换为float是一样的效果,即(float)2000000000 == 2000000050,所以f < START + 50 失败。只要把float改为int即可修正。
     没有计算器,你怎么知道 2,000,000,050 和float表示2,000,000,000一样?……
The moral of this puzzle is simple: Do not use floating-point loop indices, because it can lead to unpredictable behavior. If you need a floating-point value in the body of a loop, take the int or long loop index and convert it to a float or double. You may lose precision when converting an int or long to a float or a long to a double, but at least it will not affect the loop itself. When you use floating-point, use double rather than float unless you are certain that float provides enough precision and you have a compelling performance need to use float. The times when it's appropriate to use float rather than double are few and far between。

35
下面程序模拟一个简单的时钟
int minutes = 0;
for (int ms = 0; ms < 60*60*1000; ms++)
  if (ms %  60*1000 == 0)
     minutes++;
 
System.out.println(minutes);
结果是60000,问题在于布尔表达式ms % 60*1000 == 0,最简单的修改方法是:if (ms % (60 * 1000) == 0)
更好的方法是用合适命名的常量代替魔力数字:

private static final int MS_PER_HOUR = 60 * 60 * 1000;
private static final int MS_PER_MINUTE = 60 * 1000;
public static void main(String[] args) {
   int minutes = 0;
   for (int ms = 0; ms < MS_PER_HOUR; ms++)
     if (ms % MS_PER_MINUTE == 0)
        minutes++;
    System.out.println(minutes);
}
绝不要用空格来分组;使用括号来决定优先级

你可能感兴趣的:(Java Puzzlers(三-二 循环))