我們經(jīng)常遇到這樣的情況,,有些代碼的行為出乎意料。Java語言有很多奇怪的地方,,即使有經(jīng)驗(yàn)的開發(fā)者也可能會(huì)感到意外,。 老實(shí)說,經(jīng)常有資歷較淺的同事來問,,“執(zhí)行這段代碼有什么樣的結(jié)果,?”,讓人措手不及,?!拔铱梢愿嬖V你,但是如果你自己找出答案,,學(xué)到的會(huì)更多”,,這是很常見的答復(fù)?,F(xiàn)在可別這么說了,可以先吸引一下他的注意力(哦……我想我看到安吉麗娜·朱莉了,,藏在我們的構(gòu)建服務(wù)器后面呢,,你可以快去看一下嗎,?),,利用這個(gè)時(shí)間,快速過一下這篇文章吧,。 本文將介紹一些Java的奇怪之處,,以幫助開發(fā)者做好更充分的準(zhǔn)備,使他們再遇到結(jié)果令人意外的代碼時(shí),,能夠很好地應(yīng)對,。 對于每個(gè)技巧,,我們都會(huì)提供一些看似簡單的代碼,但是這段代碼在編譯時(shí)或運(yùn)行時(shí)的行為就不那么直觀了,。表現(xiàn)如何,,為什么會(huì)這樣,我們會(huì)講清楚背后的原理,。這些例子的復(fù)雜性不同,,有的非常簡單,有的則很費(fèi)腦細(xì)胞,。 不可理喻的標(biāo)識(shí)符我們很熟悉定義合法的Java標(biāo)識(shí)符的規(guī)則:
規(guī)則非常簡單,,但有些有趣的例子會(huì)讓人驚訝,。比如,開發(fā)者可以將類名用作標(biāo)識(shí)符,,這是沒有限制的: //類名可以用作標(biāo)識(shí)符 String String = "String"; Object Object = null; Integer Integer = new Integer(1); //讓代碼難以理解怎么樣,? Float Double = 1.0f; Double Float = 2.0d; if (String instanceof String) { if (Float instanceof Double) { if (Double instanceof Float) { System.out.print("Can anyone read this code???"); } } } 下面的標(biāo)識(shí)符也都是合法的: int $ =1; int € = 2; int £ = 3; int _ = 4; long $€£ = 5; long €_£_$ = 6; long $€£$€£$€£$€£$€£$€£$€_________$€£$€£$€£$€£$€£$€£$€£$€£$€£_____ = 7; 此外,,請記住,同樣的名字可以同時(shí)用于變量和標(biāo)簽,。通過分析上下文,,編譯器知道引用的是哪一個(gè)。 int £ = 1; £: for (int € = 0; € < £; €++) { if (€ == £) { break £; } } 當(dāng)然,,不要忘了標(biāo)識(shí)符的規(guī)則可以應(yīng)用于變量名,、方法名、標(biāo)簽和類名: class $ {} interface _ {} class € extends $ implements _ {} 所以我們學(xué)到了很厲害的一招,,那就是可以編寫沒有人能理解的代碼,,包括我們自己! NullPointerException從何而來,?自動(dòng)裝箱是在Java 5中引入的,,給我們帶來了很多方便,我們不用在基本類型和其包裝器類型之間跳來跳去了: int primitiveA = 1; Integer wrapperA = primitiveA; wrapperA++; primitiveA = wrapperA; 運(yùn)行時(shí)并沒有為了支持這種變化而做修改,,大部分工作都是編譯時(shí)完成的,。對于前面這段代碼,編譯器會(huì)生成類似下面這樣的代碼: int primitiveA = 1; Integer wrapperA = new Integer(primitiveA); int tmpPrimitiveA = wrapperA.intValue(); tmpPrimitiveA++; wrapperA = new Integer(tmpPrimitiveA); primitiveA = wrapperA.intValue(); 前面的自動(dòng)裝箱也可以應(yīng)用于方法調(diào)用: public static int calculate(int a) { int result = a + 3; return result; } public static void main(String args[]) { int i1 = 1; Integer i2 = new Integer(1); System.out.println(calculate(i1)); System.out.println(calculate(i2)); } 真棒,,對于以基本類型為參數(shù)的方法,,我們可以向其傳遞相應(yīng)的包裝器類型,讓編譯器來執(zhí)行變換: public static void main(String args[]) { int i1 = 1; Integer i2 = new Integer(1); System.out.println(calculate(i1)); int i2Tmp = i2.intValue(); System.out.println(calculate(i2Tmp)); } 稍作修改,,再來試試: public static void main(String args[]) { int i1 = 1; Integer i2 = new Integer(1); Integer i3 = null; System.out.println(calculate(i1)); System.out.println(calculate(i2)); System.out.println(calculate(i3)); } 和前面一樣,,這段代碼會(huì)被翻譯成: public static void main(String args[]) { int i1 = 1; Integer i2 = new Integer(1); Integer i3 = null; System.out.println(calculate(i1)); int i2Tmp = i2.intValue(); System.out.println(calculate(i2Tmp)); int i3Tmp = i3.intValue(); System.out.println(calculate(i3Tmp)); } 當(dāng)然,這段代碼會(huì)讓我們看到老朋友NullPointerException,。像下面這種更簡單的情況,,同樣如此: public static void main(String args[]) { Integer iW = null; int iP = iW; } 所以在使用自動(dòng)拆箱時(shí)一定要非常小心,它可能導(dǎo)致NullPointerException,;而在該特性引入之前,,是不可能遇到此類異常的。更糟糕的是,,識(shí)別這些代碼模式有時(shí)并不容易,。如果必須將一個(gè)包裝器類型的變量轉(zhuǎn)換成基本類型變量,而且不確定其是否可能為null,,那就要為代碼做好保護(hù)措施,。 包裝器類型遭遇同一性危機(jī)繼續(xù)自動(dòng)裝箱這個(gè)話題,看一下下面的代碼: Short s1 = 1; Short s2 = s1; System.out.println(s1 == s2); 當(dāng)然打印true了?,F(xiàn)在來點(diǎn)有趣的: Short s1 = 1; Short s2 = s1; s1++; System.out.println(s1 == s2); 輸出成了false,。等等,什么情況,?難道s1和s2引用的不是同一個(gè)對象嗎,?JVM真是瘋了,!還是用前面提到的代碼翻譯機(jī)制來看看吧: Short s1 = new Short((short)1); Short s2 = s1; short tempS1 = s1.shortValue(); tempS1++; s1 = new Short(tempS1); System.out.println(s1 == s2); 哦……這么看是更合理了,不是嗎,?使用自動(dòng)裝箱的時(shí)候總得小心,! 媽媽快看,沒有異常,!下面這個(gè)非常簡單,,但是很多有經(jīng)驗(yàn)的Java開發(fā)者都會(huì)中招。閑話少說,,看代碼: NullTest myNullTest = null; System.out.println(myNullTest.getInt()); 當(dāng)看到這段代碼時(shí),,很多人會(huì)以為會(huì)出現(xiàn)NullPointerException,。果真如此嗎?看看其余代碼再說: class NullTest { public static int getInt() { return 1; } } 永遠(yuǎn)記住,,類變量和類方法的使用,僅僅依賴引用的類型,。即使引用為null,,仍然可以調(diào)用,。從良好實(shí)踐的角度來看,明智的做法是使用NullTest.getInt()來代替myNullTest.getInt(),,但鬼知道什么時(shí)候會(huì)碰上這樣的代碼。 變長參數(shù)和數(shù)組,,必要的變通變長參數(shù)特性帶來了一個(gè)強(qiáng)大的概念,可以幫助開發(fā)者簡化代碼,。不過變長參數(shù)的背后是什么呢?不多不少,,就是一個(gè)數(shù)組,。 public void calc(int... myInts) {} calc(1, 2, 3); 編譯器會(huì)將前面的代碼翻譯成類似這樣: int[] ints = {1, 2, 3}; calc(ints); 當(dāng)心空調(diào)用語句,,這相當(dāng)于傳遞了一個(gè)null作為參數(shù)。 calc(); 等價(jià)于 int[] ints = null; calc(ints); 當(dāng)然,,下面的代碼會(huì)導(dǎo)致編譯錯(cuò)誤,,因?yàn)閮蓷l語句是等價(jià)的: public void m1(int[] myInts) { ... } public void m1(int... myInts) { ... } 可變的常量大部分開發(fā)者認(rèn)為,當(dāng)變量定義中出現(xiàn)final關(guān)鍵字時(shí),,指示的就是一個(gè)常量,也就是說,,這個(gè)變量的值不可改變,。這并不完全正確,,當(dāng)final關(guān)鍵字應(yīng)用于變量時(shí),只是說明該變量只能賦值一次,。 class MyClass { private final int myVar; private int myOtherVar = getMyVar(); public MyClass() { myVar = 10; } public int getMyVar() { return myVar; } public int getMyOtherVar() { return myOtherVar; } public static void main(String args[]) { MyClass mc = new MyClass(); System.out.println(mc.getMyVar()); System.out.println(mc.getMyOtherVar()); } } 前面的代碼將打印10 0。因此,,在處理final變量時(shí),,必須區(qū)分兩種情況:一種是在編譯時(shí)就賦了默認(rèn)值的,這種就是常量,;另一種是在運(yùn)行時(shí)初始化的,。 覆蓋的特色請記住,從Java 5開始,,覆蓋方法的返回類型可以與被覆蓋方法不同,。唯一的規(guī)則是,覆蓋方法的返回類型是被覆蓋方法的返回類型的子類型,。所以在Java 5中下面的代碼成了合法的: class A { public A m() { return new A(); } } class B extends A { public B m() { return new B(); } } 重載操作符就操作符重載而言,,Java不是特別強(qiáng),但它確實(shí)支持+操作符的重載,。該操作符可以用于算術(shù)加法和字符串連接,,具體取決于上下文。 int val = 1 + 2; String txt = "1" + "2"; 當(dāng)字符串中混入了數(shù)值類型,,事情就復(fù)雜了,。但是規(guī)則很簡單,在遇到字符串操作數(shù)之前,,會(huì)一直執(zhí)行算術(shù)加法。一出現(xiàn)字符串,,兩個(gè)操作數(shù)都會(huì)被轉(zhuǎn)為字符串(如果需要的話),,并執(zhí)行一次字符串連接。下面例子說明了不同的組合: System.out.println(1 + 2); //執(zhí)行加法,打印3 System.out.println("1" + "2"); //執(zhí)行字符串連接,,打印12 System.out.println(1 + 2 + 3 + "4" + 5); //執(zhí)行加法,,直到發(fā)現(xiàn)"4",然后執(zhí)行字符串連接,,打印645 System.out.println("1" + "2" + "3" + 4 + 5); //執(zhí)行字符串連接,,打印12345 奇怪的日期格式這個(gè)花招與DateFormat的實(shí)現(xiàn)有關(guān),其使用方式有一定的誤導(dǎo)性,,而且有的時(shí)候,,代碼到了產(chǎn)品中,問題才會(huì)暴露出來,。 DateFormat 的parse方法會(huì)解析一個(gè)字符串,,并生成一個(gè)日期。解析過程是根據(jù)定義的日期格式掩碼來工作的,。根據(jù)JavaDoc,如果指定的字符串的開頭部分無法解析,,會(huì)拋出一個(gè)ParseException。這個(gè)定義很模糊,,可以有不同的解釋。大部分開發(fā)者認(rèn)為,,如果字符串參數(shù)與定義的格式不匹配,,會(huì)拋出ParseException。但情況并非總是如此,。 對于SimpleDateFormat,,大家應(yīng)該非常小心。當(dāng)面對下面的代碼時(shí),,大部分開發(fā)者認(rèn)為會(huì)拋出ParseException,。 String date = "16-07-2009"; SimpleDateFormat sdf = new SimpleDateFormat("ddmmyyyy"); try { Date d = sdf.parse(date); System.out.println(DateFormat.getDateInstance(DateFormat.MEDIUM, new Locale("US")).format(d)); } catch (ParseException pe) { System.out.println("Exception: " + pe.getMessage()); } 運(yùn)行這段代碼,會(huì)產(chǎn)生下列輸出:Jan 16, 0007,。真是奇怪,,編譯器竟然沒有指出字符串與預(yù)期的格式不匹配,,而是繼續(xù)處理,而且盡其最大努力來解析文本,。請注意,,這里有兩個(gè)隱藏的花招。其一,,月份的掩碼是MM,,而mm用于分鐘,,這就解釋了為什么月份被設(shè)置成了一月。其二,,DecimalFormat類的parse方法將一直解析文本,,直到遇到無法解析的字符,返回的是到目前這個(gè)位置已經(jīng)處理過的數(shù)字,。所以“7-20”將翻譯成7年,。這種差異很容易看出來,但如果使用的是“yyyymmdd”,,情況就更復(fù)雜了,,輸出將是“Jan 7, 0016”。解析“16-0”,,直到遇到第一個(gè)不可解析的字符,,所以16會(huì)被當(dāng)成年份?!?0”不會(huì)影響結(jié)果,,它會(huì)被理解為0分鐘,。之后“7-”就被映射到天數(shù)了。 譯者注:文中關(guān)于自動(dòng)裝箱的說明不夠準(zhǔn)確,,像“Integer wrapperA = primitiveA;”這條語句,,編譯器的處理策略是將其映射為“Integer wrapperA = Integer.valueOf(primitive);”,Short的處理類似,。有興趣的讀者可以自行測試。 另外,,對Java謎題感興趣的讀者可以閱讀Joshua Bloch的《Java解惑》一書,其中列出了很多容易出錯(cuò)的地方,。 關(guān)于作者Paulo Moreira是葡萄牙的一位自由軟件工程師,目前在盧森堡的財(cái)政部門工作,。他畢業(yè)于米尼奧大學(xué),獲得了計(jì)算機(jī)科學(xué)和系統(tǒng)工程的碩士學(xué)位,。從2001年起,,他一直使用Java,,從事服務(wù)器端的開發(fā)工作,涉及電信,、零售,、軟件和金融市場等領(lǐng)域,。 |
|