蒐集到10個最難的面試題

努力小張發表於2020-10-17

1.為什麼等待和通知是在 Object 類而不是 Thread 中宣告的?
2.為什麼Java中不支援多重繼承?
3.為什麼Java不支援運算子過載?
4.為什麼 String 在 Java 中是不可變的?
5.為什麼 char 陣列比 Java 中的 String 更適合儲存密碼?
6.如何使用雙重檢查鎖定在 Java 中建立執行緒安全的單例?
7. 編寫 Java 程式時, 如何在 Java 中建立死鎖並修復它?
8. 如果你的Serializable類包含一個不可序列化的成員,會發生什麼?你是如何解決的?
9. 為什麼Java中 wait 方法需要在 synchronized 的方法中呼叫?
10.你能用Java覆蓋靜態方法嗎?如果我在子類中建立相同的方法是編譯時錯誤?
這是我收集的10個最棘手的Java面試問題列表。這些問題主要來自 Java 核心部分 ,不涉及 Java EE 相關問題。你可能知道這些棘手的 Java 問題的答案,或者覺得這些不足以挑戰你的 Java 知識,但這些問題都是容易在各種 Java 面試中被問到的,而且包括我的朋友和同事在內的許多程式設計師都覺得很難回答。

1.為什麼等待和通知是在 Object 類而不是 Thread 中宣告的?

一個棘手的 Java 問題,如果 Java程式語言不是你設計的,你怎麼能回答這個問題呢。Java程式設計的常識和深入瞭解有助於回答這種棘手的 Java 核心方面的面試問題。

為什麼 wait,notify 和 notifyAll 是在 Object 類中定義的而不是在 Thread 類中定義

這是有名的 Java 面試問題,招2~4年經驗的到高階 Java 開發人員面試都可能碰到。

這個問題的好在它能反映了面試者對等待通知機制的瞭解, 以及他對此主題的理解是否明確。就像為什麼 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 的另一個原因。

  3. 在 Java 中為了進入程式碼的臨界區,執行緒需要鎖定並等待鎖定,他們不知道哪些執行緒持有鎖,而只是知道鎖被某個執行緒持有, 並且他們應該等待取得鎖, 而不是去了解哪個執行緒在同步塊內,並請求它們釋放鎖定。

  4. Java 是基於 Hoare 的監視器的思想。在Java中,所有物件都有一個監視器。

執行緒在監視器上等待,為執行等待,我們需要2個引數:

一個執行緒
一個監視器(任何物件)
在 Java 設計中,執行緒不能被指定,它總是執行當前程式碼的執行緒。但是,我們可以指定監視器(這是我們稱之為等待的物件)。這是一個很好的設計,因為如果我們可以讓任何其他執行緒在所需的監視器上等待,這將導致“入侵”,導致在設計併發程式時會遇到困難。請記住,在 Java 中,所有在另一個執行緒的執行中侵入的操作都被棄用了(例如 stop 方法)。

2.為什麼Java中不支援多重繼承?

我發現這個 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面試題大全,可以在Java知音公眾號回覆“面試題聚合”)

3.為什麼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++。

4.為什麼 String 在 Java 中是不可變的?

我最喜歡的 Java 面試問題,很棘手,但同時也非常有用。一些面試者也常問這個問題,為什麼 String 在 Java 中是 final 的。

字串在 Java 中是不可變的,因為 String 物件快取在 String 池中。由於快取的字串在多個客戶之間共享,因此始終存在風險,其中一個客戶的操作會影響所有其他客戶。例如,如果一段程式碼將 String “Test” 的值更改為 “TEST”,則所有其他客戶也將看到該值。由於 String 物件的快取效能是很重要的一方面,因此通過使 String 類不可變來避免這種風險。

同時,String 是 final 的,因此沒有人可以通過擴充套件和覆蓋行為來破壞 String 類的不變性、快取、雜湊值的計算等。String 類不可變的另一個原因可能是由於 HashMap。

由於把字串作為 HashMap 鍵很受歡迎。對於鍵值來說,重要的是它們是不可變的,以便用它們檢索儲存在 HashMap 中的值物件。由於 HashMap 的工作原理是雜湊,因此需要具有相同的值才能正常執行。如果在插入後修改了 String 的內容,可變的 String將在插入和檢索時生成兩個不同的雜湊碼,可能會丟失 Map 中的值物件。

如果你是印度板球迷,你可能能夠與我的下一句話聯絡起來。字串是Java的 VVS Laxman,即非常特殊的類。我還沒有看到一個沒有使用 String 編寫的 Java 程式。這就是為什麼對 String 的充分理解對於 Java 開發人員來說非常重要。

String 作為資料型別,傳輸物件和中間人角色的重要性和流行性也使這個問題在 Java 面試中很常見。

為什麼 String 在 Java 中是不可變的是 Java 中最常被問到的字串訪問問題之一,它首先討論了什麼是 String,Java 中的 String 如何與 C 和 C++ 中的 String 不同,然後轉向在Java中什麼是不可變物件,不可變物件有什麼好處,為什麼要使用它們以及應該使用哪些場景。

這個問題有時也會問:“為什麼 String 在 Java 中是 final 的”。在類似的說明中,如果你正在準備Java 面試,我建議你看看《Java程式設計師面試寶典(第4版) 》,這是高階和中級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中是最終的”。要想是不可變的,你必須是最終的,這樣你的子類不會破壞不變性。你怎麼看?

5.為什麼 char 陣列比 Java 中的 String 更適合儲存密碼?

另一個基於 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知音公眾號回覆“面試題聚合”)

6.如何使用雙重檢查鎖定在 Java 中建立執行緒安全的單例?

這個 Java 問題也常被問: 什麼是執行緒安全的單例,你怎麼建立它。好吧,在Java 5之前的版本, 使用雙重檢查鎖定建立單例 Singleton 時,如果多個執行緒試圖同時建立 Singleton 例項,則可能有多個 Singleton 例項被建立。從 Java 5 開始,使用 Enum 建立執行緒安全的Singleton很容易。但如果面試官堅持雙重檢查鎖定,那麼你必須為他們編寫程式碼。記得使用volatile變數。

為什麼列舉單例在 Java 中更好

列舉單例是使用一個例項在 Java 中實現單例模式的新方法。雖然Java中的單例模式存在很長時間,但列舉單例是相對較新的概念,在引入Enum作為關鍵字和功能之後,從Java5開始在實踐中。本文與之前關於 Singleton 的內容有些相關, 其中討論了有關 Singleton 模式的面試中的常見問題, 以及 10 個 Java 列舉示例, 其中我們看到了如何通用列舉可以。這篇文章是關於為什麼我們應該使用Eeame作為Java中的單例,它比傳統的單例方法相比有什麼好處等等。

Java 列舉和單例模式

Java 中的列舉單例模式是使用列舉在 Java 中實現單例模式。單例模式在 Java 中早有應用, 但使用列舉型別建立單例模式時間卻不長. 如果感興趣, 你可以瞭解下構建者設計模式和裝飾器設計模式。

  1. 列舉單例易於書寫

這是迄今為止最大的優勢,如果你在Java 5之前一直在編寫單例, 你知道, 即使雙檢查鎖定, 你仍可以有多個例項。雖然這個問題通過 Java 記憶體模型的改進已經解決了, 從 Java 5 開始的 volatile 型別變數提供了保證, 但是對於許多初學者來說, 編寫起來仍然很棘手。與同步雙檢查鎖定相比,列舉單例實在是太簡單了。如果你不相信, 那就比較一下下面的傳統雙檢查鎖定單例和列舉單例的程式碼:

在 Java 中使用列舉的單例

這是我們通常宣告列舉的單例的方式,它可能包含例項變數和例項方法,但為了簡單起見,我沒有使用任何例項方法,只是要注意,如果你使用的例項方法且該方法能改變物件的狀態的話, 則需要確保該方法的執行緒安全。預設情況下,建立列舉例項是執行緒安全的,但 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() 來獲取此類的訪問許可權。

  1. 列舉單例自行處理序列化

傳統單例的另一個問題是,一旦實現可序列化介面,它們就不再是 Singleton, 因為 readObject() 方法總是返回一個新例項, 就像 Java 中的建構函式一樣。通過使用 readResolve() 方法, 通過在以下示例中替換 Singeton 來避免這種情況:

//readResolve to prevent another instance of Singletonprivate Object readResolve(){ return INSTANCE;}
如果 Singleton 類保持內部狀態, 這將變得更加複雜, 因為你需要標記為 transient(不被序列化),但使用列舉單例, 序列化由 JVM 進行。

  1. 建立列舉例項是執行緒安全的

如第 1 點所述,因為 Enum 例項的建立在預設情況下是執行緒安全的, 你無需擔心是否要做雙重檢查鎖定。

總之, 在保證序列化和執行緒安全的情況下,使用兩行程式碼列舉單例模式是在 Java 5 以後的世界中建立 Singleton 的最佳方式。你仍然可以使用其他流行的方法, 如你覺得更好, 歡迎討論。

  1. 編寫 Java 程式時, 如何在 Java 中建立死鎖並修復它?

經典但核心Java面試問題之一。

如果你沒有參與過多執行緒併發 Java 應用程式的編碼,你可能會失敗。

如何避免 Java 執行緒死鎖?

如何避免 Java 中的死鎖?是 Java 面試的熱門問題之一, 也是多執行緒的程式設計中的重口味之一, 主要在招高階程式設計師時容易被問到, 且有很多後續問題。儘管問題看起來非常基本, 但大多數 Java 開發人員一旦你開始深入, 就會陷入困境。

面試問題總是以“什麼是死鎖?”開始

當兩個或多個執行緒在等待彼此釋放所需的資源(鎖定)並陷入無限等待即是死鎖。它僅在多工或多執行緒的情況下發生。

如何檢測 Java 中的死鎖?

雖然這可以有很多答案, 但我的版本是首先我會看看程式碼, 如果我看到一個巢狀的同步塊,或從一個同步的方法呼叫其他同步方法, 或試圖在不同的物件上獲取鎖, 如果開發人員不是非常小心,就很容易造成死鎖。

另一種方法是在執行應用程式時實際鎖定時找到它, 嘗試採取執行緒轉儲,在 Linux 中,你可以通過kill -3命令執行此操作, 這將列印應用程式日誌檔案中所有執行緒的狀態, 並且你可以看到哪個執行緒被鎖定在哪個執行緒物件上。

你可以使用 fastthread.io 網站等工具分析該執行緒轉儲, 這些工具允許你上載執行緒轉儲並對其進行分析。

另一種方法是使用 jConsole 或 VisualVM, 它將顯示哪些執行緒被鎖定以及哪些物件被鎖定。

如果你有興趣瞭解故障排除工具和分析執行緒轉儲的過程, 我建議你看看 Uriah Levy 在多元視覺(PluraIsight)上《分析 Java 執行緒轉儲》課程。旨在詳細瞭解 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中的死鎖?

現在面試官來到最後一部分, 在我看來, 最重要的部分之一; 如何修復程式碼中的死鎖?或如何避免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 鎖以繼續。(實用詳盡的Java面試題大全,可以在Java知音公眾號回覆“面試題聚合”)

  1. 如果你的Serializable類包含一個不可序列化的成員,會發生什麼?你是如何解決的?

任何序列化該類的嘗試都會因NotSerializableException而失敗,但這可以通過在 Java中 為 static 設定瞬態(trancient)變數來輕鬆解決。

Java 序列化相關的常見問題

Java 序列化是一個重要概念, 但它很少用作永續性解決方案, 開發人員大多忽略了 Java 序列化 API。根據我的經驗, Java 序列化在任何 Java核心內容面試中都是一個相當重要的話題, 在幾乎所有的網面試中, 我都遇到過一兩個 Java 序列化問題, 我看過一次面試, 在問幾個關於序列化的問題之後候選人開始感到不自在, 因為缺乏這方面的經驗。

他們不知道如何在 Java 中序列化物件, 或者他們不熟悉任何 Java 示例來解釋序列化, 忘記了諸如序列化在 Java 中如何工作, 什麼是標記介面, 標記介面的目的是什麼, 瞬態變數和可變變數之間的差異, 可序列化介面具有多少種方法, 在 Java 中,Serializable 和 Externalizable 有什麼區別, 或者在引入註解之後, 為什麼不用 @Serializable 註解或替換 Serializalbe 介面。

在本文中,我們將從初學者和高階別進行提問, 這對新手和具有多年 Java 開發經驗的高階開發人員同樣有益。

關於Java序列化的10個面試問題

大多數商業專案使用資料庫或記憶體對映檔案或只是普通檔案, 來滿足永續性要求, 只有很少的專案依賴於 Java 中的序列化過程。無論如何,這篇文章不是 Java 序列化教程或如何序列化在 Java 的物件, 但有關序列化機制和序列化 API 的面試問題, 這是值得去任何 Java 面試前先看看以免讓一些未知的內容驚到自己。

對於那些不熟悉 Java 序列化的人, Java 序列化是用來通過將物件的狀態儲存到帶有.ser副檔名的檔案來序列化 Java 中的物件的過程, 並且可以通過這個檔案恢復重建 Java物件狀態, 這個逆過程稱為 deserialization。

什麼是 Java 序列化

序列化是把物件改成可以存到磁碟或通過網路傳送到其他執行中的 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) 序列化時,你希望某些成員不要序列化?你如何實現它?

另一個經常被問到的序列化面試問題。這也是一些時候也問, 如什麼是瞬態 trasient 變數, 瞬態和靜態變數會不會得到序列化等,所以,如果你不希望任何欄位是物件的狀態的一部分, 然後宣告它靜態或瞬態根據你的需要, 這樣就不會是在 Java 序列化過程中被包含在內。

問題 5) 如果類中的一個成員未實現可序列化介面, 會發生什麼情況?

關於Java序列化過程的一個簡單問題。如果嘗試序列化實現可序列化的類的物件,但該物件包含對不可序列化類的引用,則在執行時將引發不可序列化異常 NotSerializableException, 這就是為什麼我始終將一個可序列化警報(在我的程式碼註釋部分中), 程式碼註釋最佳實踐之一, 指示開發人員記住這一事實, 在可序列化類中新增新欄位時要注意。

問題 6) 如果類是可序列化的, 但其超類不是, 則反序列化後從超級類繼承的例項變數的狀態如何?

Java 序列化過程僅在物件層次都是可序列化結構中繼續, 即實現 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, 並確保在單個類中始終保持不變。

  1. Java序列化機制中的相容更改和不相容更改是什麼?

真正的挑戰在於通過新增任何欄位、方法或刪除任何欄位或方法來更改類結構, 方法是使用已序列化的物件。根據 Java 序列化規範, 新增任何欄位或方法都面臨相容的更改和更改類層次結構或取消實現的可序列化介面, 有些介面在非相容更改下。對於相容和非相容更改的完整列表, 我建議閱讀 Java 序列化規範。

  1. 我們可以通過網路傳輸一個序列化的物件嗎?

是的 ,你可以通過網路傳輸序列化物件, 因為 Java 序列化物件仍以位元組的形式保留, 位元組可以通過網路傳送。你還可以將序列化物件儲存在磁碟或資料庫中作為 Blob。

  1. 在 Java 序列化期間,哪些變數未序列化?

這個問題問得不同, 但目的還是一樣的, Java開發人員是否知道靜態和瞬態變數的細節。由於靜態變數屬於類, 而不是物件, 因此它們不是物件狀態的一部分, 因此在 Java 序列化過程中不會儲存它們。由於 Java 序列化僅保留物件的狀態,而不是物件本身。瞬態變數也不包含在 Java 序列化過程中, 並且不是物件的序列化狀態的一部分。在提出這個問題之後,面試官會詢問後續內容, 如果你不儲存這些變數的值, 那麼一旦對這些物件進行反序列化並重新建立這些變數, 這些變數的價值是多少?這是你們要考慮的。

  1. 為什麼Java中 wait 方法需要在 synchronized 的方法中呼叫?

另一個棘手的核心 Java 問題,wait 和 notify。它們是在有 synchronized 標記的方法或 synchronized 塊中呼叫的,因為 wait 和 modify 需要監視對其上呼叫 wait 或 notify-get 的 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訪談的幾乎所有重要主題,例如核心Java,多執行緒,IO 和 NIO 以及 Spring 和 Hibernate 等框架。你可以在這裡檢視。

為什麼要等待來自 Java中的 synchronized 方法的 wait方法為什麼必須從 Java 中的 synchronized 塊或方法呼叫 ?我們主要使用 wait(),notify() 或 notifyAll() 方法用於 Java 中的執行緒間通訊。一個執行緒在檢查條件後正在等待,例如,在經典的生產者 - 消費者問題中,如果緩衝區已滿,則生產者執行緒等待,並且消費者執行緒通過使用元素在緩衝區中建立空間後通知生產者執行緒。呼叫notify()或notifyAll()方法向單個或多個執行緒發出一個條件已更改的通知,並且一旦通知執行緒離開 synchronized 塊,正在等待的所有執行緒開始獲取正在等待的物件鎖定,幸運的執行緒在重新獲取鎖之後從 wait() 方法返回並繼續進行。

讓我們將整個操作分成幾步,以檢視Java中wait()和notify()方法之間的競爭條件的可能性,我們將使用Produce Consumer 執行緒示例更好地理解方案:

Producer 執行緒測試條件(緩衝區是是否完整)並確認必須等待(找到緩衝區已滿)。
Consumer 執行緒在使用緩衝區中的元素後設定條件。
Consumer 執行緒呼叫 notify() 方法; 這是不會被聽到的,因為 Producer 執行緒還沒有等待。
Producer 執行緒呼叫 wait() 方法並進入等待狀態。
因此,由於競態條件,我們可能會丟失通知,如果我們使用緩衝區或只使用一個元素,生產執行緒將永遠等待,你的程式將掛起。“在java同步中等待 notify 和 notifyall 現在讓我們考慮如何解決這個潛在的競態條件?

這個競態條件通過使用 Java 提供的 synchronized 關鍵字和鎖定來解決。為了呼叫 wait(),notify() 或 notifyAll(), 在Java中,我們必須獲得對我們呼叫方法的物件的鎖定。由於 Java 中的 wait() 方法在等待之前釋放鎖定並在從 wait() 返回之前重新獲取鎖定方法,我們必須使用這個鎖來確保檢查條件(緩衝區是否已滿)和設定條件(從緩衝區獲取元素)是原子的,這可以通過在 Java 中使用 synchronized 方法或塊來實現。

我不確定這是否是面試官實際期待的,但這個我認為至少有意義,請糾正我如果我錯了,請告訴我們是否還有其他令人信服的理由呼叫 wait(),notify() 或 Java 中的 notifyAll() 方法。

總結一下,我們用 Java 中的 synchronized 方法或 synchronized 塊呼叫 Java 中的 wait(),notify() 或 notifyAll() 方法來避免:

  1. Java 會丟擲 IllegalMonitorStateException,如果我們不呼叫來自同步上下文的wait(),notify()或者notifyAll()方法。

  2. Javac 中 wait 和 notify 方法之間的任何潛在競爭條件。

10.你能用Java覆蓋靜態方法嗎?如果我在子類中建立相同的方法是編譯時錯誤?

不,你不能在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進行繫結。如果要覆蓋靜態mehtod,則會呼叫子類或 ColorScreen 中的方法。這一切都在討論中我們可以覆蓋Java中的靜態方法。我們已經確認沒有,我們不能覆蓋靜態方法,我們只能在Java中隱藏靜態方法。建立具有相同名稱和mehtod簽名的靜態方法稱為Java隱藏方法。IDE將顯示警告:“靜態方法應該使用類名而不是使用物件來呼叫”, 因為靜態方法不能在Java中重寫。

相關文章