上一篇記錄了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,變得更聰明。下一章是循環之謎,會更好玩。
