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,字符串是最有意思的話題,今天到這。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM