這是我收集的10個較難回答的 Java 面試題。這些問題主要來自 Java 核心部分 ,不涉及 Java EE 相關問題。這些問題都是容易在各類 Java 面試中被問到的。html
一個較難回答的 Java 問題, Java 編程語言又不是你設計的,你如何回答這個問題呢? 須要對 Java 編程的常識進行深刻了解才行。java
這個問題的好在它能反映面試者是否對 wait - notify 機制有沒有了解, 以及他相關知識的理解是否明確。就像爲何 Java 中不支持多繼承或者爲何 String 在 Java 中是 final 的問題同樣,這個問題也可能有多個答案。程序員
爲何在 Object 類中定義 wait 和 notify 方法,每一個人都能說出一些理由。 從個人面試經驗來看, wait 和 nofity 仍然是大多數Java 程序員最困惑的,特別是2到3年的開發人員,若是他們要求使用 wait 和 notify, 他們會很困惑。所以,若是你去參加 Java 面試,請確保對 wait 和 notify 機制有充分的瞭解,而且能夠輕鬆地使用 wait 來編寫代碼,並經過「生產者-消費者」問題或實現阻塞隊列等了解通知的機制。面試
爲何等待和通知須要從同步塊或方法中調用, 以及 Java 中的 wait,sleep 和 yield 方法之間的差別,若是你尚未讀過相關知識,必定要看看。爲什麼 wait,notify 和 notifyAll 屬於 Object 類? 爲何它們不該該在 Thread 類中? 如下觀點我認爲是有道理的:數據庫
1) wait 和 notify 不只僅是普通方法或同步工具,更重要的是它們是 Java 中兩個線程之間的通訊機制。對語言設計者而言, 若是不能經過 Java 關鍵字(例如 synchronized)實現通訊此機制,同時又要確保這個機制對每一個對象可用, 那麼 Object 類則是的合理的聲明位置。記住同步和等待通知是兩個不一樣的領域,不要把它們當作是相同的或相關的。同步是提供互斥並確保 Java 類的線程安全,而 wait 和 notify 是兩個線程之間的通訊機制。編程
2) 每一個對象均可上鎖,這是在 Object 類而不是 Thread 類中聲明 wait 和 notify 的另外一個緣由。segmentfault
3) 在 Java 中,爲了進入代碼的臨界區,線程須要鎖定並等待鎖,他們不知道哪些線程持有鎖,而只是知道鎖被某個線程持有, 而且須要等待以取得鎖, 而不是去了解哪一個線程在同步塊內,並請求它們釋放鎖。 設計模式
4) Java 是基於 Hoare 的監視器的思想(http://en.wikipedia.org/wiki/...。在Java中,全部對象都有一個監視器。數組
線程在監視器上等待,爲執行等待,咱們須要2個參數:緩存
在 Java 設計中,線程不能被指定,它老是運行當前代碼的線程。可是,咱們能夠指定監視器(這是咱們稱之爲等待的對象)。這是一個很好的設計,由於若是咱們可讓任何其餘線程在所需的監視器上等待,這將致使「入侵」,影響線程執行順序,致使在設計併發程序時會遇到困難。請記住,在 Java 中,全部在另外一個線程的執行中形成入侵的操做都被棄用了(例如 Thread.stop 方法)。
我發現這個 Java 核心問題很難回答,由於你的答案可能不會讓面試官滿意,在大多數狀況下,面試官正在尋找答案中的關鍵點,若是你提到這些關鍵點,面試官會很高興。在 Java 中回答這種棘手問題的關鍵是準備好相關主題, 以應對後續的各類可能的問題。
這是很是經典的問題,與爲何 String 在 Java 中是不可變的很相似; 這兩個問題之間的類似之處在於它們主要是由 Java 創做者的設計決策使然。
爲何Java不支持多重繼承, 能夠考慮如下兩點:
1) 第一個緣由是圍繞鑽石💎形繼承問題產生的歧義,考慮一個類 A 有 foo() 方法, 而後 B 和 C 派生自 A, 而且有本身的 foo() 實現,如今 D 類使用多個繼承派生自 B 和C,若是咱們只引用 foo(), 編譯器將沒法決定它應該調用哪一個 foo()。這也稱爲 Diamond 問題,由於這個繼承方案的結構相似於菱形,見下圖:
A foo() / \ / \ foo() B C foo() \ / \ / D foo()
即便咱們刪除鑽石的頂部 A 類並容許多重繼承,咱們也將看到這個問題含糊性的一面。若是你把這個理由告訴面試官,他會問爲何 C++ 能夠支持多重繼承而 Java不行。嗯,在這種狀況下,我會試着向他解釋我下面給出的第二個緣由,它不是由於技術難度, 而是更多的可維護和更清晰的設計是驅動因素, 雖然這隻能由 Java 言語設計師確認,咱們只是推測。維基百科連接有一些很好的解釋,說明在使用多重繼承時,因爲鑽石問題,不一樣的語言地址問題是如何產生的。
2) 對我來講第二個也是更有說服力的理由是,多重繼承確實使設計複雜化並在強制轉換、構造函數連接等過程當中產生問題。假設你須要多重繼承的狀況並很少,簡單起見,明智的決定是省略它。此外,Java 能夠經過使用接口支持單繼承來避免這種歧義。因爲接口只有方法聲明並且沒有提供任何實現,所以只有一個特定方法的實現,所以不會有任何歧義。
另外一個相似的 Java 面試難題。爲何 C++ 支持運算符重載而 Java 不支持? 有人可能會說 +
運算符在 Java 中已被重載用於字符串鏈接,不要被這些論據所欺騙。
與 C++ 不一樣,Java 不支持運算符重載。Java 不能爲程序員提供自由的標準算術運算符重載,例如+
, -
,*
和/
等。若是你之前用過 C++,那麼 Java 與 C++ 相比少了不少功能,例如 Java 不支持多重繼承,Java中沒有指針,Java中沒有地址引用傳遞。另外一個相似的問題是關於 Java 經過引用傳遞,這主要表現爲 Java 是經過值仍是引用傳參。雖然我不知道背後的真正緣由,但我認爲如下說法有些道理,爲何 Java 不支持運算符重載。
1) 簡單性和清晰性。清晰性是Java設計者的目標之一。設計者不是隻想複製語言,而是但願擁有一種清晰,真正面向對象的語言。添加運算符重載比沒有它確定會使設計更復雜,而且它可能致使更復雜的編譯器, 或減慢 JVM,由於它須要作額外的工做來識別運算符的實際含義,並減小優化的機會, 以保證 Java 中運算符的行爲。
2) 避免編程錯誤。Java 不容許用戶定義的運算符重載,由於若是容許程序員進行運算符重載,將爲同一運算符賦予多種含義,這將使任何開發人員的學習曲線變得陡峭,事情變得更加混亂。據觀察,當語言支持運算符重載時,編程錯誤會增長,從而增長了開發和交付時間。因爲 Java 和 JVM 已經承擔了大多數開發人員的責任,如在經過提供垃圾收集器進行內存管理時,由於這個功能增長污染代碼的機會, 成爲編程錯誤之源, 所以沒有多大意義。
3) JVM複雜性。從JVM的角度來看,支持運算符重載使問題變得更加困難。經過更直觀,更乾淨的方式使用方法重載也能實現一樣的事情,所以不支持 Java 中的運算符重載是有意義的。與相對簡單的 JVM 相比,複雜的 JVM 可能致使 JVM 更慢,併爲保證在 Java 中運算符行爲的肯定性從而減小了優化代碼的機會。
4) 讓開發工具處理更容易。這是在 Java 中不支持運算符重載的另外一個好處。省略運算符重載後使語言更容易處理,如靜態分析等,這反過來又更容易開發處理語言的工具,例如 IDE 或重構工具。Java 中的重構工具遠勝於 C++。
我最喜歡的 Java 面試問題,很差回答,但同時也很是有用。一些面試者也常問這個問題,爲何 String 在 Java 中是 final
的。
字符串在 Java 中是不可變的,由於 String 對象緩存在 String 池中。因爲緩存的字符串在多個客戶之間共享,所以始終存在風險,其中一個客戶的操做會影響全部其餘客戶。例如,若是一段代碼將 String 「Test」 的值更改成 「TEST」,則全部其餘客戶也將看到該值。因爲 String 對象的緩存是性能的重要保證,所以經過使 String 類不可變來避免這種風險。
同時,String 是 final 的,所以沒有人能夠經過擴展和覆蓋行爲來破壞 String 類的不變性、緩存、散列值的計算等。String 類不可變的另外一個緣由多是因爲 HashMap
。
因爲把字符串做爲 HashMap 鍵很受歡迎。對於鍵值來講,不可變性是很是的重要,以便用它們檢索存儲在 HashMap 中的值對象。因爲 HashMap 的工做原理是散列,所以須要具備相同的值才能正常運行。若是在插入後修改了 String 的內容,可變的 String 將在插入和檢索時生成兩個不一樣的哈希碼,可能會丟失 Map 中的值對象。
字符串是Java 很是特殊的類。我尚未看到一個沒有使用 String 編寫的 Java 程序。這就是爲何對 String 的充分理解對於 Java 開發人員來講很是重要。
String 是數據類型,也傳輸對象和中間人。 這種多重角色的重要性和流行性, 也使這個問題在 Java 面試中很常見。
爲何 String 在 Java 中是不可變的是 Java 中最常被問到的字符串訪問問題之一,它首先討論了什麼是 String,Java 中的 String 如何與 C 和 C++ 中的 String 不一樣,而後轉向在Java中什麼是不可變對象,不可變對象有什麼好處,爲何要使用它們以及應該使用哪些場景。這個問題有時也會問:「爲何 String 在 Java 中是 final 的」。在相似的說明中,若是你正在準備Java 面試,我建議你看看Java編程面試公開書,這是高級和中級Java程序員的優秀資源。它包含來自全部重要 Java 主題的問題,包括多線程,集合,GC,JVM內部以及 Spring和 Hibernate 框架等。
正如我所說,這個問題可能有不少可能的答案,而 String 類的惟一設計者能夠放心地回答它。我在 Joshua Bloch 的 Effective Java 書中期待一些線索,但他也沒有提到它。我認爲如下幾點解釋了爲何 String 類在 Java 中是不可變的或 final 的:
1) 想象字符串池沒有使字符串不可變,它根本不可能,由於在字符串池的狀況下,一個字符串對象/文字,例如 「Test」 已被許多參考變量引用,所以若是其中任何一個更改了值,其餘參數將自動受到影響,即假設
String A="Test"; String B="Test";
如今字符串 B 調用 "Test".toUpperCase()
, 將同一個對象改成「TEST」,因此 A 也是 「TEST」,這不是指望的結果。
下圖顯示瞭如何在堆內存和字符串池中建立字符串。
2) 字符串已被普遍用做許多 Java 類的參數,例如,爲了打開網絡鏈接,你能夠將主機名和端口號做爲字符串傳遞,你能夠將數據庫 URL 做爲字符串傳遞, 以打開數據庫鏈接,你能夠經過將文件名做爲參數傳遞給 File I/O 類來打開 Java 中的任何文件。若是 String 不是不可變的,這將致使嚴重的安全威脅,個人意思是有人能夠訪問他有權受權的任何文件,而後能夠故意或意外地更改文件名並得到對該文件的訪問權限。因爲不變性,你無需擔憂這種威脅。這個緣由也說明了,爲何 String 在 Java 中是最終的,經過使 java.lang.String
final,Java設計者確保沒有人覆蓋 String 類的任何行爲。
3) 因爲 String 是不可變的,它能夠安全地共享許多線程,這對於多線程編程很是重要. 而且避免了 Java 中的同步問題,不變性也使得String 實例在 Java 中是線程安全的,這意味着你不須要從外部同步 String 操做。關於 String 的另外一個要點是由截取字符串 SubString 引發的內存泄漏,這不是與線程相關的問題,但也是須要注意的。
4) 爲何 String 在 Java 中是不可變的另外一個緣由是容許 String 緩存其哈希碼,Java 中的不可變 String 緩存其哈希碼,而且不會在每次調用 String 的 hashcode 方法時從新計算,這使得它在 Java 中的 HashMap 中使用的 HashMap 鍵很是快。簡而言之,由於 String 是不可變的,因此沒有人能夠在建立後更改其內容,這保證了 String 的 hashCode 在屢次調用時是相同的。
5) String 不可變的絕對最重要的緣由是它被類加載機制使用,所以具備深入和基本的安全考慮。若是 String 是可變的,加載「java.io.Writer」 的請求可能已被更改成加載 「mil.vogoon.DiskErasingWriter」. 安全性和字符串池是使字符串不可變的主要緣由。順便說一句,上面的理由很好回答另外一個Java面試問題: 「爲何String在Java中是最終的」。要想是不可變的,你必須是最終的,這樣你的子類不會破壞不變性。你怎麼看?
另外一個基於 String 的棘手 Java 問題,相信我只有不多的 Java 程序員能夠正確回答這個問題。這是一個真正艱難的核心Java面試問題,而且須要對 String 的紮實知識才能回答這個問題。
這是最近在 Java 面試中向個人一位朋友詢問的問題。他正在接受技術主管職位的面試,而且有超過6年的經驗。若是你尚未遇到過這種狀況,那麼字符數組和字符串能夠用來存儲文本數據,可是選擇一個而不是另外一個很難。但正如個人朋友所說,任何與 String 相關的問題都必須對字符串的特殊屬性有一些線索,好比不變性,他用它來講服訪提問的人。在這裏,咱們將探討爲何你應該使用char[]
存儲密碼而不是String
的一些緣由。
字符串:1)因爲字符串在 Java 中是不可變的,若是你將密碼存儲爲純文本,它將在內存中可用,直到垃圾收集器清除它. 而且爲了可重用性,會存在 String 在字符串池中, 它極可能會保留在內存中持續很長時間,從而構成安全威脅。
因爲任何有權訪問內存轉儲的人均可以以明文形式找到密碼,這是另外一個緣由,你應該始終使用加密密碼而不是純文本。因爲字符串是不可變的,因此不能更改字符串的內容,由於任何更改都會產生新的字符串,而若是你使用char[]
,你就能夠將全部元素設置爲空白或零。所以,在字符數組中存儲密碼能夠明顯下降竊取密碼的安全風險。
2)Java 自己建議使用 JPasswordField
的 getPassword()
方法,該方法返回一個 char[]
和不推薦使用的getTex()
方法,該方法以明文形式返回密碼,因爲安全緣由。應遵循 Java 團隊的建議, 堅持標準而不是反對它。
3)使用 String 時,老是存在在日誌文件或控制檯中打印純文本的風險,但若是使用 Array,則不會打印數組的內容而是打印其內存位置。雖然不是一個真正的緣由,但仍然有道理。
String strPassword =「Unknown」; char [] charPassword = new char [] {'U','n','k','w','o','n'}; System.out.println(「字符密碼:」+ strPassword); System.out.println(「字符密碼:」+ charPassword);
輸出
字符串密碼:Unknown
字符密碼:[C @110b053
我還建議使用散列或加密的密碼而不是純文本,並在驗證完成後當即從內存中清除它。所以,在Java中,用字符數組用存儲密碼比字符串是更好的選擇。雖然僅使用char[]
還不夠,還你須要擦除內容才能更安全。
艱難的核心 Java 面試問題.這個 Java 問題也常被問: 什麼是線程安全的單例,你怎麼建立它。好吧,在Java 5
以前的版本, 使用雙重檢查鎖定建立單例 Singleton
時,若是多個線程試圖同時建立 Singleton
實例,則可能有多個 Singleton
實例被建立。從 Java 5 開始,使用 Enum 建立線程安全的Singleton
很容易。但若是面試官堅持雙重檢查鎖定,那麼你必須爲他們編寫代碼。記得使用volatile
變量。
枚舉單例是使用一個實例在 Java 中實現單例模式的新方法。雖然Java中的單例模式存在很長時間,但枚舉單例是相對較新的概念,在引入Enum做爲關鍵字和功能以後,從Java5開始在實踐中。本文與以前關於 Singleton 的內容有些相關, 其中討論了有關 Singleton
模式的面試中的常見問題, 以及 10 個 Java 枚舉示例, 其中咱們看到了如何通用枚舉能夠。這篇文章是關於爲何咱們應該使用Eeame做爲Java中的單例,它比傳統的單例方法相比有什麼好處等等。
Java 中的枚舉單例模式是使用枚舉在 Java 中實現單例模式。單例模式在 Java 中早有應用, 但使用枚舉類型建立單例模式時間卻不長. 若是感興趣, 你能夠了解下構建者設計模式和裝飾器設計模式。
1) 枚舉單例易於書寫
這是迄今爲止最大的優點,若是你在Java 5以前一直在編寫單例, 你知道, 即便雙檢查鎖定, 你仍能夠有多個實例。雖然這個問題經過 Java 內存模型的改進已經解決了, 從 Java 5 開始的 volatile
類型變量提供了保證, 可是對於許多初學者來講, 編寫起來仍然很棘手。與同步雙檢查鎖定相比,枚舉單例實在是太簡單了。若是你不相信, 那就比較一下下面的傳統雙檢查鎖定單例和枚舉單例的代碼:
這是咱們一般聲明枚舉的單例的方式,它可能包含實例變量和實例方法,但爲了簡單起見,我沒有使用任何實例方法,只是要注意,若是你使用的實例方法且該方法能改變對象的狀態的話, 則須要確保該方法的線程安全。默認狀況下,建立枚舉實例是線程安全的,但 Enum 上的任何其餘方法是否線程安全都是程序員的責任。
/** * 使用 Java 枚舉的單例模式示例 */ public enum EasySingleton{ INSTANCE; }
你能夠經過EasySingleton.INSTANCE
來處理它,這比在單例上調用getInstance()
方法容易得多。
下面的代碼是單例模式中雙重檢查鎖定的示例,此處的 getInstance()
方法檢查兩次,以查看 INSTANCE 是否爲空,這就是爲何它被稱爲雙檢查鎖定模式,請記住,雙檢查鎖定是代理以前Java 5,但Java5內存模型中易失變量的干擾,它應該工做完美。
/** * 單例模式示例,雙重鎖定檢查 */ public class DoubleCheckedLockingSingleton{ private volatile DoubleCheckedLockingSingleton INSTANCE; private DoubleCheckedLockingSingleton(){} public DoubleCheckedLockingSingleton getInstance(){ if(INSTANCE == null){ synchronized(DoubleCheckedLockingSingleton.class){ //double checking Singleton instance if(INSTANCE == null){ INSTANCE = new DoubleCheckedLockingSingleton(); } } } return INSTANCE; } }
你能夠調用DoubleCheckedLockingSingleton.getInstance()
來獲取此單例類的訪問權限。
如今,只需查看建立延遲加載的線程安全的 Singleton 所需的代碼量。使用枚舉單例模式, 你能夠在一行中具備該模式, 由於建立枚舉實例是線程安全的, 而且由 JVM 進行。
人們可能會爭辯說,有更好的方法來編寫 Singleton 而不是雙檢查鎖定方法, 但每種方法都有本身的優勢和缺點, 就像我最喜歡在類加載時建立的靜態字段 Singleton, 以下面所示, 但請記住, 這不是一個延遲加載單例:
這是我最喜歡的在 Java 中影響 Singleton 模式的方法之一,由於 Singleton 實例是靜態的,而且最後一個變量在類首次加載到內存時初始化,所以實例的建立本質上是線程安全的。
/** * 單例模式示例與靜態工廠方法 */ public class Singleton{ //initailzed during class loading private static final Singleton INSTANCE = new Singleton(); //to prevent creating another instance of Singleton private Singleton(){} public static Singleton getSingleton(){ return INSTANCE; } }
你能夠調用 Singleton.getSingleton()
來獲取此類的訪問權限。
2) 枚舉單例自行處理序列化
傳統單例的另外一個問題是,一旦實現可序列化接口,它們就再也不是 Singleton, 由於 readObject() 方法老是返回一個新實例, 就像 Java 中的構造函數同樣。經過使用 readResolve() 方法, 經過在如下示例中替換 Singeton 來避免這種狀況:
//readResolve to prevent another instance of Singleton private Object readResolve(){ return INSTANCE; }
若是 Singleton 類保持內部狀態, 這將變得更加複雜, 由於你須要標記爲 transient(不被序列化),但使用枚舉單例, 序列化由 JVM 進行。
3) 建立枚舉實例是線程安全的
如第 1 點所述,由於 Enum 實例的建立在默認狀況下是線程安全的, 你無需擔憂是否要作雙重檢查鎖定。
總之, 在保證序列化和線程安全的狀況下,使用兩行代碼枚舉單例模式是在 Java 5 之後的世界中建立 Singleton 的最佳方式。你仍然可使用其餘流行的方法, 如你以爲更好, 歡迎討論。
經典但核心Java面試問題之一。
若是你沒有參與過多線程併發 Java 應用程序的編碼,你可能會失敗。
如何避免 Java 中的死鎖?是 Java 面試的熱門問題之一, 也是多線程的編程中的重口味之一, 主要在招高級程序員時容易被問到, 且有不少後續問題。儘管問題看起來很是基本, 但大多數 Java 開發人員一旦你開始深刻, 就會陷入困境。
當兩個或多個線程在等待彼此釋放所需的資源(鎖定)並陷入無限等待便是死鎖。它僅在多任務或多線程的狀況下發生。
雖然這能夠有不少答案, 但個人版本是首先我會看看代碼, 若是我看到一個嵌套的同步塊,或從一個同步的方法調用其餘同步方法, 或試圖在不一樣的對象上獲取鎖, 若是開發人員不是很是當心,就很容易形成死鎖。
另外一種方法是在運行應用程序時實際鎖定時找到它, 嘗試採起線程轉儲,在 Linux 中,你能夠經過kill -3
命令執行此操做, 這將打印應用程序日誌文件中全部線程的狀態, 而且你能夠看到哪一個線程被鎖定在哪一個線程對象上。
你可使用 fastthread.io 網站等工具分析該線程轉儲, 這些工具容許你上載線程轉儲並對其進行分析。
另外一種方法是使用 jConsole 或 VisualVM, 它將顯示哪些線程被鎖定以及哪些對象被鎖定。
若是你有興趣瞭解故障排除工具和分析線程轉儲的過程, 我建議你看看 Uriah Levy 在多元視覺(PluraIsight)上《分析 Java 線程轉儲》課程。旨在詳細瞭解 Java 線程轉儲, 並熟悉其餘流行的高級故障排除工具。
一旦你回答了前面的問題,他們可能會要求你編寫代碼,這將致使Java死鎖。
這是個人版本之一
/** * Java 程序經過強制循環等待來建立死鎖。 * * */ public class DeadLockDemo { /* * 此方法請求兩個鎖,第一個字符串,而後整數 */ public void method1() { synchronized (String.class) { System.out.println("Aquired lock on String.class object"); synchronized (Integer.class) { System.out.println("Aquired lock on Integer.class object"); } } } /* * 此方法也請求相同的兩個鎖,但徹底 * 相反的順序,即首先整數,而後字符串。 * 若是一個線程持有字符串鎖,則這會產生潛在的死鎖 * 和其餘持有整數鎖,他們等待對方,永遠。 */ public void method2() { synchronized (Integer.class) { System.out.println("Aquired lock on Integer.class object"); synchronized (String.class) { System.out.println("Aquired lock on String.class object"); } } } }
若是 method1() 和 method2() 都由兩個或多個線程調用,則存在死鎖的可能性, 由於若是線程 1 在執行 method1() 時在 Sting 對象上獲取鎖, 線程 2 在執行 method2() 時在 Integer 對象上獲取鎖, 等待彼此釋放 Integer 和 String 上的鎖以繼續進行一步, 但這永遠不會發生。
此圖精確演示了咱們的程序, 其中一個線程在一個對象上持有鎖, 並等待其餘線程持有的其餘對象鎖。
你能夠看到, Thread1 須要 Thread2 持有的 Object2 上的鎖,而 Thread2 但願得到 Thread1 持有的 Object1 上的鎖。因爲沒有線程願意放棄, 所以存在死鎖, Java 程序被卡住。
其理念是, 你應該知道使用常見併發模式的正確方法, 若是你不熟悉這些模式,那麼 Jose Paumard 《應用於併發和多線程的常見 Java 模式》是學習的好起點。
如今面試官來到最後一部分, 在我看來, 最重要的部分之一; 如何修復代碼中的死鎖?或如何避免Java中的死鎖?
若是你仔細查看了上面的代碼,那麼你可能已經發現死鎖的真正緣由不是多個線程, 而是它們請求鎖的方式, 若是你提供有序訪問, 則問題將獲得解決。
下面是個人修復版本,它經過避免循環等待,而避免死鎖, 而不須要搶佔, 這是須要死鎖的四個條件之一。
public class DeadLockFixed { /** * 兩種方法如今都以相同的順序請求鎖,首先採用整數,而後是 String。 * 你也能夠作反向,例如,第一個字符串,而後整數, * 只要兩種方法都請求鎖定,二者都能解決問題 * 順序一致。 */ public void method1() { synchronized (Integer.class) { System.out.println("Aquired lock on Integer.class object"); synchronized (String.class) { System.out.println("Aquired lock on String.class object"); } } } public void method2() { synchronized (Integer.class) { System.out.println("Aquired lock on Integer.class object"); synchronized (String.class) { System.out.println("Aquired lock on String.class object"); } } } }
如今沒有任何死鎖,由於兩種方法都按相同的順序訪問 Integer 和 String 類文本上的鎖。所以,若是線程 A 在 Integer 對象上獲取鎖, 則線程 B 不會繼續, 直到線程 A 釋放 Integer 鎖, 即便線程 B 持有 String 鎖, 線程 A 也不會被阻止, 由於如今線程 B 不會指望線程 A 釋放 Integer 鎖以繼續。
任何序列化該類的嘗試都會因NotSerializableException
而失敗,但這能夠經過在 Java中 爲 static 設置瞬態(trancient)變量來輕鬆解決。
Java 序列化是一個重要概念, 但它不多用做持久性解決方案, 開發人員大多忽略了 Java 序列化 API。根據個人經驗, Java 序列化在任何 Java核心內容面試中都是一個至關重要的話題, 在幾乎全部的網面試中, 我都遇到過一兩個 Java 序列化問題, 我看過一次面試, 在問幾個關於序列化的問題以後候選人開始感到不自在, 由於缺少這方面的經驗。 他們不知道如何在 Java 中序列化對象, 或者他們不熟悉任何 Java 示例來解釋序列化, 忘記了諸如序列化在 Java 中如何工做, 什麼是標記接口, 標記接口的目的是什麼, 瞬態變量和可變變量之間的差別, 可序列化接口具備多少種方法, 在 Java 中,Serializable
和 Externalizable
有什麼區別, 或者在引入註解以後, 爲何不用 @Serializable
註解或替換 Serializalbe
接口。
在本文中,咱們將從初學者和高級別進行提問, 這對新手和具備多年 Java 開發經驗的高級開發人員一樣有益。
大多數商業項目使用數據庫或內存映射文件或只是普通文件, 來知足持久性要求, 只有不多的項目依賴於 Java 中的序列化過程。不管如何,這篇文章不是 Java 序列化教程或如何序列化在 Java 的對象, 但有關序列化機制和序列化 API 的面試問題, 這是值得去任何 Java 面試前先看看以避免讓一些未知的內容驚到本身。
對於那些不熟悉 Java 序列化的人, Java 序列化是用來經過將對象的狀態存儲到帶有.ser
擴展名的文件來序列化 Java 中的對象的過程, 而且能夠經過這個文件恢復重建 Java對象狀態, 這個逆過程稱爲 deserialization。
序列化是把對象改爲能夠存到磁盤或經過網絡發送到其餘運行中的 Java 虛擬機的二進制格式的過程, 並能夠經過反序列化恢復對象狀態. Java 序列化API給開發人員提供了一個標準機制, 經過 java.io.Serializable
和 java.io.Externalizable
接口, ObjectInputStream
及ObjectOutputStream
處理對象序列化. Java 程序員可自由選擇基於類結構的標準序列化或是他們自定義的二進制格式, 一般認爲後者纔是最佳實踐, 由於序列化的二進制文件格式成爲類輸出 API的一部分, 可能破壞 Java 中私有和包可見的屬性的封裝.
讓 Java 中的類能夠序列化很簡單. 你的 Java 類只須要實現 java.io.Serializable
接口, JVM 就會把 Object 對象按默認格式序列化。 讓一個類是可序列化的須要有意爲之。 類可序列會可能爲是一個長期代價, 可能會所以而限制你修改或改變其實現. 當你經過實現添加接口來更改類的結構時, 添加或刪除任何字段可能會破壞默認序列化, 這能夠經過自定義二進制格式使不兼容的可能性最小化, 但仍須要大量的努力來確保向後兼容性。序列化如何限制你更改類的能力的一個示例是 SerialVersionUID。若是不顯式聲明 SerialVersionUID, 則 JVM 會根據類結構生成其結構, 該結構依賴於類實現接口和可能更改的其餘幾個因素。 假設你新版本的類文件實現的另外一個接口, JVM 將生成一個不一樣的 SerialVersionUID 的, 當你嘗試加載舊版本的程序序列化的舊對象時, 你將得到無效類異常 InvalidClassException。
問題 1) Java 中的可序列化接口和可外部接口之間的區別是什麼?
這是 Java 序列化訪談中最常問的問題。下面是個人版本 Externalizable 給咱們提供 writeExternal() 和 readExternal() 方法, 這讓咱們靈活地控制 Java 序列化機制, 而不是依賴於 Java 的默認序列化。 正確實現 Externalizable 接口能夠顯著提升應用程序的性能。
問題 2) 可序列化的方法有多少?若是沒有方法,那麼可序列化接口的用途是什麼?
可序列化 Serializalbe 接口存在於java.io
包中,構成了 Java 序列化機制的核心。它沒有任何方法, 在 Java 中也稱爲標記接口。 當類實現 java.io.Serializable
接口時, 它將在 Java 中變得可序列化, 並指示編譯器使用 Java 序列化機制序列化此對象。
問題 3) 什麼是 serialVersionUID ?若是你不定義這個, 會發生什麼?
我最喜歡的關於Java序列化的問題面試問題之一。serialVersionUID 是一個 private static final long
型 ID, 當它被印在對象上時, 它一般是對象的哈希碼,你可使用 serialver
這個 JDK 工具來查看序列化對象的 serialVersionUID。SerialVerionUID 用於對象的版本控制。 也能夠在類文件中指定 serialVersionUID。不指定 serialVersionUID的後果是,當你添加或修改類中的任何字段時, 則已序列化類將沒法恢復, 由於爲新類和舊序列化對象生成的 serialVersionUID 將有所不一樣。Java 序列化過程依賴於正確的序列化對象恢復狀態的, ,並在序列化對象序列版本不匹配的狀況下引起 java.io.InvalidClassException
無效類異常,瞭解有關 serialVersionUID 詳細信息,請參閱這篇文章, 須要 FQ。
問題 4) 序列化時,你但願某些成員不要序列化?你如何實現它?
另外一個常常被問到的序列化面試問題。這也是一些時候也問, 如什麼是瞬態 transient 變量, 瞬態和靜態變量會不會獲得序列化等,因此,若是你不但願任何字段是對象的狀態的一部分, 而後聲明它靜態或瞬態根據你的須要, 這樣就不會是在 Java 序列化過程當中被包含在內。
問題 5) 若是類中的一個成員未實現可序列化接口,會發生什麼狀況?
關於Java 序列化過程的一個簡單問題。若是嘗試序列化_實現了可序列化接口的類_的對象,但該對象包含對不可序列化類的引用,則在運行時將引起不可序列化異常 NotSerializableException
, 這就是爲何我始終將一個可序列化警報(在個人代碼註釋部分中),做爲代碼註釋最佳實踐之一, 提示開發人員記住這一事實, 在可序列化類中添加新字段時要注意。
問題 6) 若是類是可序列化的, 但其超類不是, 則反序列化後從超級類繼承的實例變量的狀態如何?
Java 序列化過程僅在對象層級都是_可序列化_的類中繼續, 即:實現了可序列化接口, 若是從超級類沒有實現可序列化接口,則超級類繼承的實例變量的值將經過調用構造函數初始化。且一旦構造函數鏈啓動, 就不可能中止, 所以, 即便層次結構中更高的類成員變量實現了可序列化接口, 也將經過執行構造函數建立,而再也不是反序列化獲得。如你所見, 這個序列化面試問題看起來很是不易回答, 但若是你熟悉關鍵概念, 則並不難。
問題 7) 是否能夠自定義序列化過程, 或者是否能夠覆蓋 Java 中的默認序列化過程?
答案是確定的, 你能夠。咱們都知道,對於序列化一個對象需調用 ObjectOutputStream.writeObject(saveThisObject)
, 並用 ObjectInputStream.readObject()
讀取對象, 但 Java 虛擬機爲你提供的還有一件事, 是定義這兩個方法。 若是在類中定義這兩種方法, 則 JVM 將調用這兩種方法, 而不是應用默認序列化機制。 你能夠在此處經過執行任何類型的預處理或後處理任務來自定義對象序列化和反序列化的行爲。須要注意的重要一點是要聲明這些方法爲私有方法, 以免被繼承、重寫或重載。 因爲只有 Java 虛擬機能夠調用類的私有方法, 你的類的完整性會獲得保留, 而且 Java 序列化將正常工做。在我看來, 這是在任何 Java 序列化面試中能夠問的最好問題之一, 一個很好的後續問題是, 爲何要爲你的對象提供自定義序列化表單?
問題 8) 假設新類的超級類實現可序列化接口, 如何避免新類被序列化?
這是在 Java 序列化中很差回答的問題。若是類的 Super 類已經在 Java 中實現了可序列化接口, 那麼它在 Java 中已經能夠序列化, 由於你不能取消接口,它不可能真正使它沒法序列化類, 可是有一種方法能夠避免新類序列化。爲了不 Java 序列化,你須要在類中實現 writeObject()
和 readObject()
方法, 而且須要從該方法引起不序列化異常NotSerializableException
。 這是自定義 Java 序列化過程的另外一個好處, 如上述序列化面試問題中所述, 而且一般隨着面試進度, 它做爲後續問題提出。
問題 9) 在 Java 中的序列化和反序列化過程當中使用哪些方法?
這是很常見的面試問題, 在序列化基本上面試官試圖知道: 你是否熟悉 readObject()
的用法、writeObject()
、readExternal()
和 writeExternal()
。 Java 序列化由java.io.ObjectOutputStream
類完成。 該類是一個篩選器流, 它封裝在較低級別的字節流中, 以處理序列化機制。要經過序列化機制存儲任何對象, 咱們調用 ObjectOutputStream.writeObject(savethisobject)
, 並反序列化該對象, 咱們稱之爲 ObjectInputStream.readObject()
方法。調用以 writeObject()
方法在 java 中觸發序列化過程。關於 readObject()
方法, 須要注意的一點很重要一點是, 它用於從持久性讀取字節, 並從這些字節建立對象, 並返回一個對象, 該對象須要類型強制轉換爲正確的類型。
問題 10) 假設你有一個類,它序列化並存儲在持久性中, 而後修改了該類以添加新字段。若是對已序列化的對象進行反序列化, 會發生什麼狀況?
這取決於類是否具備其本身的 serialVersionUID。正如咱們從上面的問題知道, 若是咱們不提供 serialVersionUID, 則 Java 編譯器將生成它, 一般它等於對象的哈希代碼。經過添加任何新字段, 有可能爲該類新版本生成的新 serialVersionUID 與已序列化的對象不一樣, 在這種狀況下, Java 序列化 API 將引起 java.io.InvalidClassException
, 所以建議在代碼中擁有本身的 serialVersionUID, 並確保在單個類中始終保持不變。
11) Java序列化機制中的兼容更改和不兼容更改是什麼?
真正的挑戰在於經過添加任何字段、方法或刪除任何字段或方法來更改類結構, 方法是使用已序列化的對象。根據 Java 序列化規範, 添加任何字段或方法都面臨兼容的更改和更改類層次結構或取消實現的可序列化接口, 有些接口在非兼容更改下。對於兼容和非兼容更改的完整列表, 我建議閱讀 Java 序列化規範。
12) 咱們能夠經過網絡傳輸一個序列化的對象嗎?
是的 ,你能夠經過網絡傳輸序列化對象, 由於 Java 序列化對象仍以字節的形式保留, 字節能夠經過網絡發送。你還能夠將序列化對象存儲在磁盤或數據庫中做爲 Blob。
13) 在 Java 序列化期間,哪些變量未序列化?
這個問題問得不一樣, 但目的仍是同樣的, Java開發人員是否知道靜態和瞬態變量的細節。因爲靜態變量屬於類, 而不是對象, 所以它們不是對象狀態的一部分, 所以在 Java 序列化過程當中不會保存它們。因爲 Java 序列化僅保留對象的狀態,而不是對象自己。瞬態變量也不包含在 Java 序列化過程當中, 而且不是對象的序列化狀態的一部分。在提出這個問題以後,面試官會詢問後續內容, 若是你不存儲這些變量的值, 那麼一旦對這些對象進行反序列化並從新建立這些變量, 這些變量的值是多少?這是大家要考慮的。
另外一個有難度的 Java 問題,wait 和 notify。它們是在有 synchronized 標記的方法或 synchronized 塊中調用的,由於 wait 和 nodify 須要監視對其調用的 Object。
大多數Java開發人員都知道對象類的 wait(),notify() 和 notifyAll() 方法必須在 Java 中的 synchronized 方法或 synchronized 塊中調用, 可是咱們想過多少次, 爲何在 Java 中 wait, notify 和 notifyAll 來自 synchronized 塊或方法?
最近這個問題在Java面試中被問到個人一位朋友,他思索了一下,並回答說: 若是咱們不從同步上下文中調用 wait() 或 notify() 方法,咱們將在 Java 中收到 IllegalMonitorStateException。
他的回答從實際效果上是正確的,但面試官對這樣的答案不會徹底滿意,並但願向他解釋這個問題。面試結束後他和我討論了這個問題,我認爲他應該告訴面試官關於 Java 中 wait()和 notify()之間的競態條件,若是咱們不在同步方法或塊中調用它們就可能存在。
讓咱們看看競態條件如何在 Java 程序中產生。它也是流行的線程面試問題之一。所以,若是你正在準備Java面試,那麼你應該準備這樣的問題,而且能夠真正幫助你的一本書是《Java程序員面試公開書》的。這是一本罕見的書,涵蓋了Java訪談的幾乎全部重要主題,例如核心Java,多線程,IO 和 NIO 以及 Spring 和 Hibernate 等框架。
爲何要等待來自 Java中的 synchronized 方法的 wait方法爲何必須從 Java 中的 synchronized 塊或方法調用 ? 咱們主要使用 wait(),notify() 或 notifyAll() 方法用於 Java 中的線程間通訊。一個線程在檢查條件後正在等待,例如,在經典的生產者 - 消費者問題中,若是緩衝區已滿,則生產者線程等待,而且消費者線程經過使用元素在緩衝區中建立空間後通知生產者線程。調用notify() 或 notifyAll() 方法向單個或多個線程發出一個條件已更改的通知,而且一旦通知線程離開 synchronized 塊,正在等待的全部線程開始獲取正在等待的對象鎖定,幸運的線程在從新獲取鎖以後從 wait() 方法返回並繼續進行。
讓咱們將整個操做分紅幾步,以查看Java 中 wait() 和 notify() 方法之間的競爭條件的可能性,咱們將使用Produce Consumer 線程示例更好地理解方案:
所以,因爲競態條件,咱們可能會丟失通知,若是咱們使用緩衝區或只使用一個元素,生產線程將永遠等待,你的程序將掛起。「在Java 同步中等待 notify 和 notifyAll 如今讓咱們考慮如何解決這個潛在的競態條件?這個競態條件經過使用 Java 提供的 synchronized 關鍵字和鎖定來解決。爲了調用 wait(),notify() 或 notifyAll(),必須得到對咱們調用方法的對象的鎖定。因爲 Java 中的 wait() 方法在等待以前釋放鎖定並在從 wait() 返回以前從新獲取鎖定方法,咱們必須使用這個鎖來確保檢查條件(緩衝區是否已滿) 和設置條件 (從緩衝區獲取元素) 是原子的,這能夠經過使用 synchronized 方法或塊來實現。
我不肯定這是不是面試官實際期待的,但這個我認爲至少有意義,請糾正我若是我錯了,請告訴咱們是否還有其餘使人信服的理由調用 wait(),notify() 或 Java 中的 notifyAll() 方法。
總結一下,咱們用 Java 中的 synchronized 方法或 synchronized 塊調用 Java 中的 wait(),notify() 或 notifyAll() 方法來避免:
1) Java 會拋出 IllegalMonitorStateException
,若是咱們不調用來自同步上下文的wait(),notify()或者notifyAll()方法。
2) Javac 中 wait 和 notify 方法之間的任何潛在競爭條件。
不,你不能在Java中覆蓋靜態方法,但在子類中聲明一個徹底相同的方法不是編譯時錯誤,這稱爲隱藏在Java中的方法。
你不能覆蓋Java中的靜態方法,由於方法覆蓋基於運行時的動態綁定,靜態方法在編譯時使用靜態綁定進行綁定。雖然能夠在子類中聲明一個具備相同名稱和方法簽名的方法,看起來能夠在Java中覆蓋靜態方法,但實際上這是方法隱藏。Java不會在運行時解析方法調用,而且根據用於調用靜態方法的 Object 類型,將調用相應的方法。這意味着若是你使用父類的類型來調用靜態方法,那麼原始靜態將從父類中調用,另外一方面若是你使用子類的類型來調用靜態方法,則會調用來自子類的方法。簡而言之,你沒法在Java中覆蓋靜態方法。若是你使用像Eclipse或Netbeans這樣的Java IDE,它們將顯示警告靜態方法應該使用類名而不是使用對象來調用,由於靜態方法不能在Java中重寫。
/** * * Java program which demonstrate that we can not override static method in Java. * Had Static method can be overridden, with Super class type and sub class object * static method from sub class would be called in our example, which is not the case. */ public class CanWeOverrideStaticMethod { public static void main(String args[]) { Screen scrn = new ColorScreen(); //if we can override static , this should call method from Child class scrn.show(); //IDE will show warning, static method should be called from classname } } class Screen{ /* * public static method which can not be overridden in Java */ public static void show(){ System.out.printf("Static method from parent class"); } } class ColorScreen extends Screen{ /* * static method of same name and method signature as existed in super * class, this is not method overriding instead this is called * method hiding in Java */ public static void show(){ System.err.println("Overridden static method in Child Class in Java"); } }
輸出:
Static method from parent class
此輸出確認你沒法覆蓋 Java 中的靜態方法,而且靜態方法基於類型信息而不是基於 Object 進行綁定。若是要覆蓋靜態方法,則會調用子類或 ColorScreen 中的方法。這一切都在討論中咱們能夠覆蓋 Java 中的靜態方法。咱們已經確認沒有,咱們不能覆蓋靜態方法,咱們只能在Java中隱藏靜態方法。建立具備相同名稱和方法簽名的靜態方法稱爲Java 隱藏方法。IDE 將顯示警告:"靜態方法應該使用類名而不是使用對象來調用", 由於靜態方法不能在 Java 中重寫。
原文發表於: https://segmentfault.com/a/11...
這些是個人核心 Java 面試問題和答案的清單。對於有經驗的程序員來講,一些 Java 問題看起來並不那麼難,但對於Java中的中級和初學者來講,它們真的很難回答。順便說一句,若是你在面試中遇到任何棘手的Java問題,請與咱們分享。
Read more: http://www.java67.com/2012/09...