八成Java開發(fā)者解答不了的問題
統(tǒng)計數(shù)據(jù)來自Java“死亡”競賽——一個針對開發(fā)者的迷你測驗
幾個月前,我們在一個小型網(wǎng)站上發(fā)布了一個稱為Java“死亡競賽”的新項目。測驗發(fā)布后,超過20000位開發(fā)者參加了測驗。網(wǎng)站以20道關(guān)于Java的多選題為主。我們得到了眾多開發(fā)者的測驗統(tǒng)計數(shù)據(jù),今天,我們非常樂意將其中的一些數(shù)據(jù)和答案與你們分享。
我們從20個題目中得到了61872個答案,大約每個題目有3094個答案。每個Java“死亡”測驗都會隨機地從20個題目中抽取5個題目,然后每個題目有90 秒的時間作答。每個問題有四個可能的選項。經(jīng)常有人向我們抱怨說這些題目太難了。所以,我們的測驗被稱為Java“死亡”競賽并不是沒有理由的哦!從測驗 結(jié)果的統(tǒng)計數(shù)據(jù)中,我們能知道哪些問題是最難的,哪些是最簡單的。在這篇博客中,我想與你們分享5個從我們的測驗中挑選出的最難的問題,然后一起解決它 們。
平均來看,開發(fā)者給出的答案中大約41%是正確的,這個結(jié)果可一點不差。每個問題的索引和它的作答統(tǒng)計結(jié)果可以從這里得到。這篇博客所用的統(tǒng)計數(shù)據(jù)是在7月26日得到的。從這里可以嘗試我們的Java“死亡”競賽測驗。
1、Java“死亡競賽”中最難的問題
讓我們從最難啃的骨頭開始吧。這個問題由來自羅馬尼亞首都布加勒斯特的 Alexandru-Constantin Bledea提供。這個問題確實是一個腦筋急轉(zhuǎn)彎,只有約20%的參與者答對這道題,這意味著瞎選都能提高你回答正確的概率。這道題是關(guān)于Java泛型的。
題目大意:
這段代碼錯在哪兒?
a.編譯錯誤,因為沒有SQLException被拋出
b.拋出ClassCastException,因為SQLException并不是RuntimeException的一個實例
c.沒有錯誤,程序打印出拋出的SQLException堆棧跟蹤信息
d.編譯錯誤,因為我們不能將SQLException類型轉(zhuǎn)換成RuntimeException
好,我們能從題目中得到什么信息?題目中的泛型涉及到了類型擦除,以及一些異常。這里需要回憶一些知識:
RuntimeException和SQLException都繼承自Exception,但是在這個代碼中RuntimeException是未檢查的異常,而SQLException是受檢異常。
2.Java的泛型并不是具體化的。這意味著在編譯時,泛型的類型信息會“丟失”,并且泛型參數(shù)像是被它的限定類型替換了一樣,或者當(dāng)限定類型不存在時,泛型參數(shù)被替換成了Object。這就是大家所說的類型“擦除”。
我們天真地希望第七行能產(chǎn)生一個編譯錯誤,因為我們不能將SQLException轉(zhuǎn)換成RuntimeException,但是這并不會發(fā)生。發(fā)生的是將T替換成了Exception,所以我們有:
throw (Exception) t; // t is also an Exception
pleaseThrow方法期望一個Exception,并且T被替換成了Exception,因此類型轉(zhuǎn)換被擦除了,就像沒寫這個代碼一樣。這一點我們可從下面的字節(jié)碼中得到佐證:
private pleaseThrow(Ljava/lang/Exception;)V throws java/lang/Exception
L0
LINENUMBER 8 L0
ALOAD 1
ATHROW
L1
LOCALVARIABLE this LTemp; L0 L1 0
// signature LTemp<TT;>;
// declaration: Temp<T>
LOCALVARIABLE t Ljava/lang/Exception; L0 L1 1
MAXSTACK = 1
MAXLOCALS = 2
我們再看一下,如果代碼中沒有涉及泛型,那么編譯產(chǎn)生的字節(jié)碼是什么樣的,我們看到,在ATHROW前會有如下的代碼:
CHECKCAST java/lang/RuntimeException
現(xiàn)在,我們可以確信,代碼中并沒有涉及到類型轉(zhuǎn)換,因此我們可以排除下面這兩個選項:
“編譯錯誤,因為我們不能將SQLException類型轉(zhuǎn)換為RuntimeException”
“拋出ClassCastException,因為SQLException不是RuntimeException的一個實例”
因此畢竟我們拋出了SQLException,然后你希望它能被catch代碼塊捕獲,然后打印它的堆棧跟蹤信息。然而,事與愿違。
這個代碼具有欺騙性,它使得編譯器和我們一樣變得困惑。這段代碼讓編譯器認為catch代碼塊是不能到達的。對于不知情的旁觀者來說,代碼中并沒有 SQLException。所以,正確答案是:編譯失敗,因為編譯器認為SQLException不會從try代碼塊中拋出-但是實際上它確實能拋出!
再次感謝Alexandru與我們分享這個問題!我們可以用另一個很酷的方式來查看代碼中的錯誤以及SQLException實際上是怎樣拋出的,這個方法是:修改catch代碼塊,把它修改為接收一個RuntimeException。這樣你就可以看到SQLException的堆棧信息了。(實際上SQLException也并沒有被catch代碼段捕獲,而是被虛擬機捕獲并打印出異常棧的信息。)
2、問題的關(guān)鍵在于,是否使用了toString()
這道題只有24%的正確率,它的困難程度是這20道題中的亞軍。
題目大意:這個程序的打印結(jié)果是?
a.m1 & new name
b.以上都是錯誤的
c.m1&m1
d.new name & new name
這道題實際上簡單得多,我們只要看到第十二行,它直接打印了m1和m2,而不是m1.name和m2.name。這段代碼狡猾的地方在于,當(dāng)我們要 打印一個對象時,Java使用的是toString方法。“name”屬性是我們自己加入的,如果你忘記這點,其他地方都判斷正確的話,你可能會錯誤地選 擇m1&new name這個答案。
這行代碼將兩個對象的name屬性都賦值為”m1”。
m1.name = m2.name = “m1";
然后callMe方法將m2對象的name屬性設(shè)置成”new name”,然后代碼就結(jié)束了。
但是,這個代碼片段實際上將會打印出如下信息,包括類名稱以及它們的哈希碼:
MyClass@3d0bc85 & MyClass@7d08c1b7
所以正確的答案是“None of the above”
3、Google Guava類庫中的Sets
題目大意:
這道題目不妥的地方在哪?
a.不能編譯
b.沒有問題
c.可能造成內(nèi)存溢出
d.可能造成***循環(huán)
這個問題實際上并不特別需要關(guān)于Guava sets類庫的專業(yè)知識,但卻使絕大多數(shù)的開發(fā)者產(chǎn)生困惑。只有25%的參與者給出了正確的答案,和瞎選的正確率是一樣的。
那么我們能從這段代碼中看出什么呢?我們有一個方法,它返回一個集合,這個集合包含了某個人的好友圈。方法中有一個循環(huán),它檢查一個person對 象的bestfriend屬性是否為null。如果不為null,則將bestfriend添加到results集合里。如果一個person對象確實有 一個bestfriend,那么對這個person的bestfriend,重復(fù)執(zhí)行上述過程,所以我們就可以一直向bestfriend集合添加 person對象,直到有一個person,它沒有bestfriend,或者它的bestfriend已經(jīng)在我們的result集合里了。***這部分有 一點微妙,我們不能向這個Set集合添加重復(fù)的元素,即person對象,所以這個方法并不會導(dǎo)致***循環(huán)。
真正的問題在于,這段代碼很有可能造成內(nèi)存用盡的異常(out of memory exception)。這個循環(huán)實際上是沒有邊界的,所以我們可以不停地往set中添加person對象,直到內(nèi)存用盡。
順便提一下,如果你想詳細了解Google Guava,可以看看我們寫的這篇博客: the lesser known yet useful features about it
4、利用兩個花括號進行初始化
題目大意:這段代碼錯誤的地方在哪?
a.沒有錯誤
b.可能獲得null值
c.代碼不能編譯
d.打印出不正確的結(jié)果
這個問題是代碼最少的問題之一,但是足以迷惑絕大部分的開發(fā)者。這道題只有26%的答題者回答正確。
很少有開發(fā)者知道這個初始化常量集合的簡便語法,雖然這個語法會帶來一些副作用。但事實上,這個語法鮮為人知未免不是一件好事。在感嘆之后,你看 到,我們往list里添加了一個元素,然后打印這個list。正常情況下,你期望看到打印的結(jié)果是[John],但是利用兩個花括號進行初始化是有另一套 初始化過程的。這里,我們用了一個匿名類來初始化一個List,當(dāng)要打印NAMES時,實際上打印出來的是null,這是因為初始化程序尚未完成,此時的 list是空的。
關(guān)于使用兩個花括號進行容器的初始化,可參考這里(right here)。
5、對于運行時Map容器的離奇事件
這是另一個社區(qū)貢獻的問題,貢獻者是來自以色列的Barak Yaish。只有27%的答題者能解答這個問題。
題目大意:這段代碼的輸出是什么
a.不能編譯
b.類型轉(zhuǎn)換異常
c.[] true
d.[“bar”, “ber”]
好吧,來看看代碼。compute方法通過key在map中查找一個value。如果這個value是null,則插入(key, value),并返回value。因為開始時,這個list是空的,“foo”值并不存在,v是null。然后,我們向map中插入一個“foo”并且 “foo”指向new ArrayList<Object>(),此時的ArrayList對象是空的,所以它打印出[]。
下一行,“foo”鍵值存在于map容器中,所以我們計算右邊的表達式。ArrayList對象成功轉(zhuǎn)換為List類型,然后“ber”字符串被插入到List中。add方法返回true,因此true就是第二行打印的內(nèi)容。
所以正確的答案是”[]true”。再次感謝Barak于我們分享這道題。
鼓勵一下:來看看最簡單的題吧
題目大意:哪一種方法是初始化Java字符串最簡單的方式
a.A
b.沒有一個
c.C
d.B和C不能編譯
現(xiàn)在,我們來看一下Peter Lawrey提供的問題。他工作于OpenHFT開源項目,同時也在Vanilla Java上撰寫博客。Peter在StackOverflow上排名top 50,這一次他反過來向大家提問,76%的開發(fā)者能回答出這個問題。
C答案比A簡單,B和D是不能編譯的。
結(jié)論
我們有時喜歡做這樣的小測驗來加深我們對Java知識的理解。但是, 你是否發(fā)現(xiàn)自己的代碼庫中也有這樣或那樣類似小測驗的問題使自己困惑,常常需要花許多時間來維護,這樣的話可能并不好。特別是在半夜時,你接到一個電話, 讓你去解決一個嚴重的產(chǎn)品錯誤。對于這種情況,我們開發(fā)了Takipi這個Java工具。Takipi是一個Java代理,它能在生產(chǎn)環(huán)境下追蹤未捕獲的 異常、捕獲異常以及記錄服務(wù)器上的錯誤日志。使用這個工具,你可以在堆棧中看到引發(fā)異常的變量值,然后在你的代碼中修改它們。