八成Java開發者解答不了的問題
幾個月前,我們在一個小型網站上發布了一個稱為Java“死亡競賽”的新項目。測驗發布后,超過20000位開發者參加了測驗。網站以20道關于Java的多選題為主。我們得到了眾多開發者的測驗統計數據,今天,我們非常樂意將其中的一些數據和答案與你們分享。
我們從20個題目中得到了61872個答案,大約每個題目有3094個答案。每個Java“死亡”測驗都會隨機地從20個題目中抽取5個題目,然后每個題目有90秒的時間作答。每個問題有四個可能的選項。經常有人向我們抱怨說這些題目太難了。所以,我們的測驗被稱為Java“死亡”競賽并不是沒有理由的哦!從測驗結果的統計數據中,我們能知道哪些問題是最難的,哪些是最簡單的。在這篇博客中,我想與你們分享5個從我們的測驗中挑選出的最難的問題,然后一起解決它們。
平均來看,開發者給出的答案中大約41%是正確的,這個結果可一點不差。每個問題的索引和它的作答統計結果可以從這里得到。這篇博客所用的統計數據是在7月26日得到的。從這里可以嘗試我們的Java“死亡”競賽測驗。
1、Java“死亡競賽”中最難的問題
讓我們從最難啃的骨頭開始吧。這個問題由來自羅馬尼亞首都布加勒斯特的 Alexandru-Constantin Bledea提供。這個問題確實是一個腦筋急轉彎,只有約20%的參與者答對這道題,這意味著瞎選都能提高你回答正確的概率。這道題是關于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/ExceptionL0
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</pre>
我們再看一下,如果代碼中沒有涉及泛型,那么編譯產生的字節碼是什么樣的,我們看到,在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()
![]()
這道題只有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
![]()
題目大意:
這道題目不妥的地方在哪?
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、利用兩個花括號進行初始化
![]()
題目大意:這段代碼錯誤的地方在哪?
a.沒有錯誤
b.可能獲得null值
c.代碼不能編譯
d.打印出不正確的結果
這個問題是代碼最少的問題之一,但是足以迷惑絕大部分的開發者。這道題只有26%的答題者回答正確。
很少有開發者知道這個初始化常量集合的簡便語法,雖然這個語法會帶來一些副作用。但事實上,這個語法鮮為人知未免不是一件好事。在感嘆之后,你看到,我們往list里添加了一個元素,然后打印這個list。正常情況下,你期望看到打印的結果是[John],但是利用兩個花括號進行初始化是有另一套初始化過程的。這里,我們用了一個匿名類來初始化一個List,當要打印NAMES時,實際上打印出來的是null,這是因為初始化程序尚未完成,此時的list是空的。
關于使用兩個花括號進行容器的初始化,可參考這里(right here)。
5、對于運行時Map容器的離奇事件
這是另一個社區貢獻的問題,貢獻者是來自以色列的Barak Yaish。只有27%的答題者能解答這個問題。
![]()
題目大意:這段代碼的輸出是什么
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字符串最簡單的方式
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代理,它能在生產環境下追蹤未捕獲的異常、捕獲異常以及記錄服務器上的錯誤日志。使用這個工具,你可以在堆棧中看到引發異常的變量值,然后在你的代碼中修改它們。
原文鏈接: takipi 翻譯: ImportNew.com - justyoung
譯文鏈接: http://www.importnew.com/16566.html
[ 轉載請保留原文出處、譯者和譯文鏈接。]
本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!