上一篇記錄了10個puzzle,主要是關於表示式的,表示式的一個關鍵就是有值,所以很多的謎題也都圍繞著資料型別展開,今天要分享的是字元之謎,無論什麼程式語言,字元總是一個很好玩的好題.在之前也總結過java中String的一些效能上的問題,發現看過這13個puzzle後又加深了一些理解吧。
public class LastLaugh { public static void main(String args[]) { System.out.print("H" + "a"); System.out.print('H' + 'a'); } }
這個問題還是非常簡單的,第一行肯定是列印Ha的,但是第二行就不同了,這是兩個字元相加,我們知道兩個字元相加會被提升到int型的相加,所以它實際上是72和97相加。解決的一個技巧是 System.out.print(""+'H' + 'a'); 這是把其他資料型別轉換為string的一個非常快捷的方式。
public class Abc { public static void main(String[] args) { String letters = "ABC"; char[] numbers = { '1', '2', '3' }; System.out.println(letters + " easy as " + numbers); } }
也許研究過String的同學可能認為列印numbers會理所當然的列印出字串來,因為StringBuilder這些本身也是用字元陣列來實現的,但是這個例子列印的結果是ABC easy as [C@2e6e1408 ,可以看出後面是一個物件名。原因是char陣列要轉換為string的時候要呼叫其toString方法,這個方法是從Object那裡繼承來的,所以就列印了上面的結果。但是上面的程式碼中我們如果直接列印numbers則不會出現這樣的問題,原因是System.out.println方法對於字元陣列引數進行了過載使得其可以正常列印陣列中包含的內容。
public class AnimalFarm { public static void main(String[] args) { final String pig = "length: 10"; final String dog = "length: " + pig.length(); System.out.println("Animals are equal: " + pig == dog); } }
不想賣關子,這個列印的結果就是 false(可能和大家想的差很多)。這裡面有兩個陷阱,第一個就是關於字串初始化的,這個問題反而會迷倒一些對於string有研究的同學,因為pig和dog引用的字串內容是相同的,== 比較的是引用的物件是不是同一個(C的思想是比較地址),並且根據java string 常量池的特性(一些介紹參考),任何String型別的常量表示式,如果指定的是相同的字串,那麼他們就會指向相同的物件,所以我們認為可能結果就是Animals are equal : true了。其實不然,用==判斷pig和dog會得到false。原因就是dog初始化的時候並非是一個常量表示式。忽然發現自己一直忽略了這個問題,慚愧。這個大家可以自己寫兩行簡單的程式碼測試一下。
那麼為什麼不會列印“Animals are equal:”呢?原因是操作符優先順序的問題,+的優先順序高於==,所以這個表示式實際比較的是 “Animals are euqal:length:10”和“length:10”,所以就直接列印了一個false了。這個給我們的啟示就是當一個表示式中涉及到多個操作符的時候,我們不確定優先順序的時候一定要加括號。System.out.println("Animals are equal: "+ (pig == dog));
public class EscapeRout { public static void main(String[] args) { // \u0022 is the Unicode escape for double-quote (") System.out.println("a\u0022.length() + \u0022b".length()); } }
背景介紹 \u0022 是unicode對於雙引號的表示方法。這個程式可能有兩種結果一是把列印的內容當成整個字串列印,而是先把\u0022轉義。實際上就是先進行轉移操作,這個是編譯器解析最前完成的。所以程式就變成了System.out.println("a".length() + "b".length());,。這告訴我們儘量不要用unicode轉義字元。
/** * Generated by the IBM IDL-to-Java compiler, version 1.0 * from F:\TestRoot\apps\a1\units\include\PolicyHome.idl * Wednesday, June 17, 1998 6:44:40 o'clock AM GMT+00:00 */ public class Test { public static void main(String[] args) { System.out.print("Hell"); System.out.println("o world"); } }
其實把這段程式碼直接放在你的eclipse裡面就會發現問題了,報錯,而其是註釋部分。注意第二行註釋裡面有個\u這有標誌了unicode轉移字元的開始,但是後面卻不是可識別的16進位制數,所以導致程式報錯。這個好蛋疼,是麼?
問題是很多這樣的註釋是自動生成的,和windows下目錄層級用反斜槓表示,這就很容易引發問題。所以哪天註釋報錯了,那就搜一下\u試試吧。
public class LinePrinter { public static void main(String[] args) { // Note: \u000A is Unicode representation of linefeed (LF) char c = 0x000A; System.out.println(c); } }
這個例子也比較蛋疼,原因還是在註釋裡面,這個還是和unicode轉移有關,事實上,在編譯器去掉程式碼中的空行和註釋之前,unicode的已經被替換為轉移字元了,而\u000A代表的是換行符,所以我們就可以發現問題了,這個註釋會被拆成兩行,自然就會報錯了,萬惡的unicode。
\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020\u0020 \u0063\u006c\u0061\u0073\u0073\u0020\u0055\u0067\u006c\u0079 \u007b\u0070\u0075\u0062\u006c\u0069\u0063\u0020\u0020\u0020 \u0020\u0020\u0020\u0020\u0073\u0074\u0061\u0074\u0069\u0063 \u0076\u006f\u0069\u0064\u0020\u006d\u0061\u0069\u006e\u0028 \u0053\u0074\u0072\u0069\u006e\u0067\u005b\u005d\u0020\u0020 \u0020\u0020\u0020\u0020\u0061\u0072\u0067\u0073\u0029\u007b \u0053\u0079\u0073\u0074\u0065\u006d\u002e\u006f\u0075\u0074 \u002e\u0070\u0072\u0069\u006e\u0074\u006c\u006e\u0028\u0020 \u0022\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u0022\u002b \u0022\u006f\u0072\u006c\u0064\u0022\u0029\u003b\u007d\u007d
actually。。這是一段可執行的程式碼,作者指向告訴我們,盡。。量。。不。。要。。使。。用。。unicdoe
public class StringCheese { public static void main(String args[]) { byte bytes[] = new byte[256]; for(int i = 0; i < 256; i++) bytes[i] = (byte)i; String str = new String(bytes); for(int i = 0, n = str.length(); i < n; i++) System.out.print((int)str.charAt(i) + " "); } }
這個程式首先將數字轉換為byte陣列,然後利用byte陣列生成一個String,再將string中的每一個字元轉成int列印出來,正常的思維我們想看到是0--255這些數字,但實際上是不確定的。問題出在定義一個新的String的時候,由於用bytes定義,並且沒有指定字符集,API中提到,陣列的長度是字符集的一個函式,所以如果沒有指定就出現了不確定的字元長度。
很多開發過J2EE同學應該非常熟悉另一種有byte初始化String的方法,就是傳入第二個字符集引數,在網站開發的時候經常用這個來統一編碼,特別是中文亂碼的問題。
public class Classifier { public static void main(String[] args) { System.out.println( classify('n') + classify('+') + classify('2')); } static String classify(char ch) { if ("0123456789".indexOf(ch) >= 0) return "NUMERAL "; if ("abcdefghijklmnopqrstuvwxyz".indexOf(ch) >= 0) return "LETTER "; /* * (Operators not supported yet) * if ("+-*/&|!=".indexOf(ch) >= 0) * return "OPERATOR "; */ return "UNKNOWN "; } }
這個例子的問題,其實很同意看出來了,就是塊註釋語句的第一個/*和程式碼中的*/進行了匹配,導致整個程式碼就亂了。這是塊註釋引起的一個經典的問題,書中還給了我們提示就是塊註釋是不支援巢狀的。這個puzzle中,作者還提到了一種程式設計師喜歡使用的註釋方法,就是講一段程式碼放在if(false){}的block裡面,這也容易產生問題,如果不是為了一些除錯上的方便也不建議使用。
package com.javapuzzlers; public class Me { public static void main(String[] args) { System.out.println( Me.class.getName().replaceAll(".", "/") + ".class"); } }
這個例子很簡單,他的本意是列印出這個類的名字com.javapuzzlers.Me 然後將.替換為/這樣就可以獲得這個檔案的的具體目錄了。但是要注意replaceAll的第一個引數是一個正規表示式,”.“在正則裡面,相信大家也知道表示匹配任何字元,這樣結果就全變成了//////。解決辦法有兩個,一是寫正確的正規表示式,也就是"\\."第二種方法是用Patern的quote方法,直接表示要匹配的內容。
這裡面存在一個隱患,及時我們得到想要的結果即com/javapuzzlers/Me.class 那麼它也只在unix/linux上有用,windows上的目錄是用反斜槓的,所以失效。下面的puzzle就會涉及到這個問題。
package com.javapuzzlers; import java.io.File; public class MeToo { public static void main(String[] args) { System.out.println(MeToo.class.getName(). replaceAll("\\.", File.separator) + ".class"); } }
這應該是上一個問題的修改版,在windows下會出問題,原因就是File.separator是反斜槓,而在這裡作為替代引數,和普通字串不同,他要進行轉移,所以就會發生錯誤。現在的JDK提供了replace方法,更加適合處理簡單的情況,兩個引數均為普通的字串,省去了很多的問題。
public class BrowserTest { public static void main(String[] args) { System.out.print("iexplore:"); http://www.google.com; System.out.println(":maximize"); } }
事實上,我剛發現java的這個特性,語句標號。C語言中的goto就用到過語句標號,事實上寫到這裡的時候,我還是不知道java中語句標號的作用是什麼,以及他為什麼這麼設計。這個例子中,顯然http:作為一個標號了。後面跟一行註釋,所以程式碼沒有任何問題,完全可以執行。
import java.util.*; public class Rhymes { private static Random rnd = new Random(); public static void main(String[] args) { StringBuffer word = null; switch(rnd.nextInt(2)) { case 1: word = new StringBuffer('P'); case 2: word = new StringBuffer('G'); default: word = new StringBuffer('M'); } word.append('a'); word.append('i'); word.append('n'); System.out.println(word); } }
本章最後一個puzzle是我最喜歡的,裡面三個陷阱,我只發現了一個。那就是我們發現每一個case都沒有break,所以最後的結果不可能列印Pain和Gain。那麼還有兩個陷阱,一個就是關於生成隨機數的,nextInt的引數設為2只能生成0和1兩個隨機數,正確的寫法是引數為3.
接下來是最好玩的一個陷阱,很有意思,那就是最後結果只能列印ain,很奇怪吧,事實上問題出在StringBuffer的初始化上,StringBuffer沒有字元作為引數的構造器,他只有三種構造器一是無引數的,而是接受String的,三是接受int作為初始容量的(初始化容量的詳細討論),所以這裡StringBuilder('M'),字元M會被當成int來處理,所以上面的語句相當於給StringBuilder知識初始化了容量而已。這是非常好的一個puzzle,比前面的好玩多了。
Chapter3關注的是字元之謎,其中四個puzzle涉及到了unicode轉移字元引起的問題,還有就是char和String之間的一些問題,最後一個例子是受益最深的,尤其是初始化StringBuilder那裡,給我們提了醒。不難發現,好的程式設計習慣能夠幫助我們避免很多問題,讀puzzle,變得更聰明。下一章是迴圈之謎,會更好玩。