Java解惑(一) puzzle 1--10

  把《Java解惑》这本书又从图书馆拿回来了,现在再次的重温,与大三时看这本书的时候不同,我决定写点笔记了。每天抽些时间读些puzzle来让愚钝的大脑清醒一些,读这本书的每一个puzzle的时候,感觉就像小品里面范伟饰演的角色一样,经常会说“原来是这么回事呀”。但,不同的是,读puzzle让人更聪明,而不是被忽悠。所以决定写个系列,利用这个周末到下周的几天一口气读完吧。下面的这些都是用自己的语言来描述的,完全是自己的一些理解,只是写下来记录一下自己的想法,没看过这本书的童鞋还是直接去看原版书吧。每天10个puzzle的话,那么也需要十天的时间。虽然会浪费些时间,但总比蛋疼的在微博和各种SNS上数日子强,“亲,还有XX天就世界末日了。。”。言归正传。

  第一章是表达式之谜,里面主要是利用了java语言的一些标准规范,一些错误也是由于不规范的代码写法造成的。

  puzzle 1 奇数性

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

  这是一个判断奇偶的方法,有什么不妥吗?

   当然这个是一个不完备的方法吗,没有考虑到-1的情况,负数奇数模2会得到-1,所以在这个判断中,只有正数奇数才能返回true。这里如果用0来判断会有更好的效果,就可以避免上面的错误。其实从语义的角度,0也更加的合理,我们很早以前不是就学过,奇数是不能被2整除的,而不是说“奇数是那些被2除余1的数”。书中给出的一个好的解决方案是 return (i & 1)!= 0 ,虽然看起来比较smart但是还是不如取模看上去直观。

 

  puzzle 2 找零时刻
 
        System.out.println(2.00 - 1.10);

    这个更加的精炼了,我也以为会打印出来的是 0.90,结果却是0.89999999

    原因就是double无法精确的表示诸如0.1这样的小数,说起来惭愧,我竟然忽略了这个问题。浮点的形式是1010.10101这种,同十进制类似,每一个数位都有相应的权重在里面。只看小数点后面的数位,比如0.1表示 1/2= 0.5,0.01表示1/4= 0.25 ,0.11表示1/2+1/4=0.75诸如此类,最后只能通过延长数位来无限的接近0.1这样的数值。如果想多了解这方面的知识的话,可以看下《深入理解计算机系统》这本书。Bloch给出的建议是使用java中的BigDecimal类来处理精确的浮点计算,有意思的是在看到BigDecimal类的重载构造方法BigDecimal(char[] in, int offset, int len)的时候,很有意思,这个方法实现了由一个String转换为浮点的操作,我觉得这段代码倒是非常值得学习一下,以前在微软面试的时候有过类似的例子。

 

puzzle 3 长整除
public class LongDivision {
    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);
    }
}

    这个可以简单的看出来,第一个MICROS_PER_DAY会溢出,不要被前面的long迷惑掉,这个long只有在表达式计算完成后赋值才会用,但是在整个表达式计算中,每一个常量都是int,整个过程也是按照int型来计算的,这样的空间当然就不够了。这个错误还是经常容易犯的。

 

puzzle 4 初级问题
public class Elementary {
    public static void main(String[] args) {
        System.out.println(12345 + 5432l);
    }
}

    这个问题实在太蛋疼,首先打印结果肯定不是你看起来的两个十进制数字相加,但是第二个加数实际上是5432,后面的是字母l.......这是个代码写法引发的错误,所以以后Long还是大写吧,好么?

 

puzzle 5十六进制的趣事
public class JoyOfHex {
    public static void main(String[] args) {
        System.out.println(
            Long.toHexString(0x100000000L + 0xcafebabe));
    }
}

    又是一个简单的加法,大部分人认为结果是0x1cafebabeL 但是值得注意的是,右边的加数是一个十六进制的整型,十六进制的常量是带有符号的,即最高位被置位的话就表示负数。所以0xcafebabe是一个负数,相加的时候首先要进行扩展,这样就成了0xffffffffcafebabe。

 

puzzle 6多重转换
public class Multicast {
    public static void main(String[] args) {
        System.out.println((int) (char) (byte) -1);
    }
}

     这个例子讲的是将-1这个整型经过多重转换,最后打印出来的是什么?首先经过前面那么多puzzles,我们敏感的先看一下三个数据类型的占位int 32 char 16 byte8,再根据转换顺序,可以看出我们需要先把32位的-1截断位8位,然后再扩展为16位,再扩展为32位。首先-1使用二进制补码表示的,即1111...1保留最后8位后,还是-1.char是无符号,这就变成了65535.而此时char在扩展为有符号的int,前面依旧补零,所以最后我们看到就是65535.

     书中给了一种不错的建议,当涉及到符号扩展的时候可以与一个较宽的数据类型相与,比如byte转换为char时,如果不希望符号扩展可以采用下面的操作:

     char c = (char)(b & 0xff);   这样就不会发生符号扩展了,相信道理大家都明白。

 

puzzle 7 互换内容
public class CleverSwap {
    public static void main(String[] args) {
        int x = 1984;
        int y = 2001;
        x ^= y ^= x ^= y;
        System.out.println("x = " + x + "; y = " + y);
    }
}

     看到这个代码就很不喜欢,写法很差,我想估计也只有想说明某个例子的时候才会这么写。首先背景是,我想大家都知道一个小tip 就是不使用临时变量来交换两个数。基本有两个方式比如第一种是 : a = a+b;b=a-b;a=a-b;第二种就是 a = a ^ b; b = a ^ b; a = a ^ b;后者是比较有名的,利用了一个数和自身异或为0,而任何数字和0异或还是自己。所以就有了上面的这种昔写法。。当然上面的代码是得不到正确的结果的,Bloch将上面的x ^= y ^= x ^= y分解了,我觉得这样就很明白了:首先明确两点,一是操作符从左向右求值,二是x^=y的过程是先取x的值然后,求x ^ y ,再将值赋值给x.

     所以分解一下上面的操作是(来自书中原文):

 
puzzle 8 Dos Equis
public class DosEquis {
    public static void main(String[] args) {
        char x = 'X';
        int i = 0;
        System.out.print(true  ? x : 0);
        System.out.print(false ? i : x); 
    }
}

     我表示盯着这个表达式看了很久都没有发现问题,原因是我只把 ?condition x:y当成了一种控制结构了,而忽略了这其实是一个表达式,而表达式的特点是他是有值的,这个值得类型是什么?相信到这里就可以发现问题了。这其实是一个混合类型的计算,冒号左右的两个数据类型不一致。书中介绍了一个规则,就是:

  • 如果第二个和第三个操作数具有相同的类型,那么它就是条件表达式的类型。换句话说,你可以通过绕过混合类型的计算来避免大麻烦。
  • 如果一个操作数的类型是T,T 表示byte、short 或char,而另一个操作数是一个int 类型的常量表达式,它的值是可以用类型T 表示的,那么条

件表达式的类型就是T。

  •  否则,将对操作数类型运用二进制数字提升,而条件表达式的类型就是第二个和第三个操作数被提升之后的类型。

     所以第二个表达式中,i是变量,所以x也被提升为int了,就有了这样的结果,这个puzzle还是非常不错的。

 

puzzle 9半斤
puzzl 10 八两
public class Tweedledum {
    public static void main(String[] args) {
        // Put your declarations for x and i here

        x += i;     // Must be LEGAL
        x = x + i;  // Must be ILLEGAL
    }
}

     这个例子昨天和室友赖神一起吃饭还聊到了,很有意思的两个puzzle放在一起写,要求给出x和i的声明,第一个就是需要 x += i合法,而x = x+i不合法。这个我还有一点印象,记得是和数据类型的宽窄有关,即你不能将一个宽数据类型赋给一个比他要窄的。 所以如果x是short 而 i是int ,第二个就不合法了。现在看复合赋值表达式为什么合法。。java设计事其然。“复合赋值表达式自动地将它们所执行的计算的结果转型为其左侧变量的类型”,这个时候如果左侧的更窄则隐式的进行截断。所以复合赋值表达式是危险的!

    再看另一个八两,这个我没有想到, 使得x += i不合法,而x = x+i合法。这里涉及到了一个知识点是复合赋值操作符只适用于基本类型,或者包装了基本类型的类,比如Integer。String除外,当左边是String时,右边可以是任何类型。这是不难想到一个使得x += i不合法的例子,那就是将x定义为一个Object就Ok了。而在赋值操作中,Object作为一个引用则可以指向其他类型。所以方法就是 x是Object而i是String。

 

    以上就是Chapter 2的10个puzzle,主要是和表达式的值有关,其中多次涉及到了不同宽窄数据类型之间的转换问题,数据类型占位不一给开发造成了一些麻烦,但是出于效率的考虑,似乎也有他的价值,这一点不敢多说,记得有本书中写道尽量不要用float,因为他的效率并不比double高多少,但是后者却又更高的精度。其次涉及到了一些特殊表达式容易造成的陷阱,虽然感觉有点过细了,但是还是能够让我们开阔一下视野,起码非今后调试BUG也很有帮助。

   下一章是字符puzzle,字符串是最有意思的话题,今天到这。

posted @ 2012-12-15 17:51  leeon  阅读(3063)  评论(2编辑  收藏  举报