八成Java開發者解答不了的問題

ImportNew發表於2015-08-31

統計資料來自Java“死亡”競賽——一個針對開發者的迷你測驗

八成Java開發者解答不了的問題

幾個月前,我們在一個小型網站上釋出了一個稱為Java“死亡競賽”的新專案。測驗釋出後,超過20000位開發者參加了測驗。網站以20道關於Java的多選題為主。我們得到了眾多開發者的測驗統計資料,今天,我們非常樂意將其中的一些資料和答案與你們分享。

我們從20個題目中得到了61872個答案,大約每個題目有3094個答案。每個Java“死亡”測驗都會隨機地從20個題目中抽取5個題目,然後每個題目90秒的時間作答。每個問題有四個可能的選項。經常有人向我們抱怨說這些題目太難了。所以,我們的測驗被稱為Java“死亡”競賽並不是沒有理由的哦!從測驗結果的統計資料中,我們能知道哪些問題是最難的,哪些是最簡單的。在這篇部落格中,我想與你們分享5個從我們的測驗中挑選出的最難的問題,然後一起解決它們。

八成Java開發者解答不了的問題

平均來看,開發者給出的答案中大約41%是正確的,這個結果可一點不差。每個問題的索引和它的作答統計結果可以從這裡得到。這篇部落格所用的統計資料是在7月26日得到的。從這裡可以嘗試我們的Java“死亡”競賽測驗。

1、Java“死亡競賽”中最難的問題

讓我們從最難啃的骨頭開始吧。這個問題由來自羅馬尼亞首都布加勒斯特的 Alexandru-Constantin Bledea提供。這個問題確實是一個腦筋急轉彎,只有約20%的參與者答對這道題,這意味著瞎選都能提高你回答正確的概率。這道題是關於Java泛型的。

八成Java開發者解答不了的問題

題目大意:

這段程式碼錯在哪兒?

a.編譯錯誤,因為沒有SQLException被丟擲

b.丟擲ClassCastException,因為SQLException並不是RuntimeException的一個例項

c.沒有錯誤,程式列印出丟擲的SQLException堆疊跟蹤資訊

d.編譯錯誤,因為我們不能將SQLException型別轉換成RuntimeException

好,我們能從題目中得到什麼資訊?題目中的泛型涉及到了型別擦除,以及一些異常。這裡需要回憶一些知識:

RuntimeException和SQLException都繼承自Exception,但是在這個程式碼中RuntimeException是未檢查的異常,而SQLException是受檢異常

2.Java的泛型並不是具體化的。這意味著在編譯時,泛型的型別資訊會“丟失”,並且泛型引數像是被它的限定型別替換了一樣,或者當限定型別不存在時,泛型引數被替換成了Object。這就是大家所說的型別“擦除”。

我們天真地希望第七行能產生一個編譯錯誤,因為我們不能將SQLException轉換成RuntimeException,但是這並不會發生。發生的是將T替換成了Exception,所以我們有:

throw (Exception) t; // t is also an Exception

pleaseThrow方法期望一個Exception,並且T被替換成了Exception,因此型別轉換被擦除了,就像沒寫這個程式碼一樣。這一點我們可從下面的位元組碼中得到佐證:

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

我們再看一下,如果程式碼中沒有涉及泛型,那麼編譯產生的位元組碼是什麼樣的,我們看到,在ATHROW前會有如下的程式碼:

CHECKCAST java/lang/RuntimeException

現在,我們可以確信,程式碼中並沒有涉及到型別轉換,因此我們可以排除下面這兩個選項:

“編譯錯誤,因為我們不能將SQLException型別轉換為RuntimeException”

“丟擲ClassCastException,因為SQLException不是RuntimeException的一個例項”

因此畢竟我們丟擲了SQLException,然後你希望它能被catch程式碼塊捕獲,然後列印它的堆疊跟蹤資訊。然而,事與願違。

這個程式碼具有欺騙性,它使得編譯器和我們一樣變得困惑。這段程式碼讓編譯器認為catch程式碼塊是不能到達的。對於不知情的旁觀者來說,程式碼中並沒有SQLException。所以,正確答案是:編譯失敗,因為編譯器認為SQLException不會從try程式碼塊中丟擲-但是實際上它確實能丟擲!

再次感謝Alexandru與我們分享這個問題!我們可以用另一個很酷的方式來檢視程式碼中的錯誤以及SQLException實際上是怎樣丟擲的,這個方法是:修改catch程式碼塊,把它修改為接收一個RuntimeException。這樣你就可以看到SQLException的堆疊資訊了。實際上SQLException也並沒有被catch程式碼段捕獲,而是被虛擬機器捕獲並列印出異常棧的資訊。

2、問題的關鍵在於,是否使用了toString()

八成Java開發者解答不了的問題

這道題只有24%的正確率,它的困難程度是這20道題中的亞軍。

題目大意:這個程式的列印結果是?

a.m1 & new name

b.以上都是錯誤的

c.m1&m1

d.new name & new name

這道題實際上簡單得多,我們只要看到第十二行,它直接列印了m1和m2,而不是m1.name和m2.name。這段程式碼狡猾的地方在於,當我們要列印一個物件時,Java使用的是toString方法。“name”屬性是我們自己加入的,如果你忘記這點,其他地方都判斷正確的話,你可能會錯誤地選擇m1&new name這個答案。

這行程式碼將兩個物件的name屬性都賦值為”m1”。

m1.name = m2.name = “m1";

然後callMe方法將m2物件的name屬性設定成”new name”,然後程式碼就結束了。

但是,這個程式碼片段實際上將會列印出如下資訊,包括類名稱以及它們的雜湊碼:

MyClass@3d0bc85 & MyClass@7d08c1b7

所以正確的答案是“None of the above”

3、Google Guava類庫中的Sets

八成Java開發者解答不了的問題

題目大意:

這道題目不妥的地方在哪?

a.不能編譯

b.沒有問題

c.可能造成記憶體溢位

d.可能造成無限迴圈

這個問題實際上並不特別需要關於Guava sets類庫的專業知識,但卻使絕大多數的開發者產生困惑。只有25%的參與者給出了正確的答案,和瞎選的正確率是一樣的。

那麼我們能從這段程式碼中看出什麼呢?我們有一個方法,它返回一個集合,這個集合包含了某個人的好友圈。方法中有一個迴圈,它檢查一個person物件的bestfriend屬性是否為null。如果不為null,則將bestfriend新增到results集合裡。如果一個person物件確實有一個bestfriend,那麼對這個person的bestfriend,重複執行上述過程,所以我們就可以一直向bestfriend集合新增person物件,直到有一個person,它沒有bestfriend,或者它的bestfriend已經在我們的result集合裡了。最後這部分有一點微妙,我們不能向這個Set集合新增重複的元素,即person物件,所以這個方法並不會導致無限迴圈。

真正的問題在於,這段程式碼很有可能造成記憶體用盡的異常(out of memory exception)。這個迴圈實際上是沒有邊界的,所以我們可以不停地往set中新增person物件,直到記憶體用盡。

順便提一下,如果你想詳細瞭解Google Guava,可以看看我們寫的這篇部落格: the lesser known yet useful features about it

4、利用兩個花括號進行初始化

八成Java開發者解答不了的問題

題目大意:這段程式碼錯誤的地方在哪?

a.沒有錯誤

b.可能獲得null值

c.程式碼不能編譯

d.列印出不正確的結果

這個問題是程式碼最少的問題之一,但是足以迷惑絕大部分的開發者。這道題只有26%的答題者回答正確。

很少有開發者知道這個初始化常量集合的簡便語法,雖然這個語法會帶來一些副作用。但事實上,這個語法鮮為人知未免不是一件好事。在感嘆之後,你看到,我們往list裡新增了一個元素,然後列印這個list。正常情況下,你期望看到列印的結果是[John],但是利用兩個花括號進行初始化是有另一套初始化過程的。這裡,我們用了一個匿名類來初始化一個List,當要列印NAMES時,實際上列印出來的是null,這是因為初始化程式尚未完成,此時的list是空的。

關於使用兩個花括號進行容器的初始化,可參考這裡(right here)。

5、對於執行時Map容器的離奇事件

這是另一個社群貢獻的問題,貢獻者是來自以色列的Barak Yaish。只有27%的答題者能解答這個問題。

八成Java開發者解答不了的問題

題目大意:這段程式碼的輸出是什麼

a.不能編譯

b.型別轉換異常

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物件成功轉換為List型別,然後“ber”字串被插入到List中。add方法返回true,因此true就是第二行列印的內容。

所以正確的答案是”[]true”。再次感謝Barak於我們分享這道題。

鼓勵一下:來看看最簡單的題吧

八成Java開發者解答不了的問題

題目大意:哪一種方法是初始化Java字串最簡單的方式

a.A

b.沒有一個

c.C

d.B和C不能編譯

現在,我們來看一下Peter Lawrey提供的問題。他工作於OpenHFT開源專案,同時也在Vanilla Java上撰寫部落格。Peter在StackOverflow上排名top 50,這一次他反過來向大家提問,76%的開發者能回答出這個問題。

C答案比A簡單,B和D是不能編譯的。

結論

我們有時喜歡做這樣的小測驗來加深我們對Java知識的理解但是,你是否發現自己的程式碼庫中也有這樣或那樣類似小測驗的問題使自己困惑,常常需要花許多時間來維護,這樣的話可能並不好。特別是在半夜時,你接到一個電話,讓你去解決一個嚴重的產品錯誤。對於這種情況,我們開發了Takipi這個Java工具。Takipi是一個Java代理,它能在生產環境下追蹤未捕獲的異常、捕獲異常以及記錄伺服器上的錯誤日誌。使用這個工具,你可以在堆疊中看到引發異常的變數值,然後在你的程式碼中修改它們。

相關文章