這是我收集的10個最棘手的Java面試問題清單。這些問題主要來自 Java 核心部分 ,不涉及 Java EE 相關問題。你可能知道這些棘手的Java 問題的答案,或者覺得這些不足以挑戰你的Java 知識,但這些問題都是容易在各種Java 面試中被問到的,而且包括我的朋友和同事在內的許多程式設計師都覺得很難回答。
(更多面試題推薦:java面試題目及答案)
一個棘手的 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 方法)。
我發現這個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知音公眾號回覆「面試題聚合」)
另一個類似棘手的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的 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中是最终的”。要想是不可变的,你必须是最终的,这样你的子类不会破坏不变性。你怎么看?
另一个基于 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 问题也常被问: 什么是线程安全的单例,你怎么创建它。好吧,在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() 来获取此类的访问权限。
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 中的死锁?是 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知音公众号回复“面试题聚合”)
任何序列化该类的尝试都会因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, 並確保在單一類別中始終保持不變。
11) Java序列化機制中的相容變更和不相容變更是什麼?
真正的挑戰在於透過添加任何欄位、方法或刪除任何欄位或方法來更改類別結構, 方法是使用已序列化的物件。根據 Java 序列化規範, 添加任何字段或方法都面臨兼容的更改和更改類層次結構或取消實現的可序列化接口, 有些接口在非兼容更改下。對於兼容和非兼容更改的完整列表, 我建議閱讀 Java 序列化規範。
12) 我們可以透過網路傳輸一個序列化的物件嗎?
是的 ,你可以透過網路傳輸序列化物件, 因為 Java 序列化物件仍以位元組的形式保留, 位元組可以透過網路傳送。你也可以將序列化物件儲存在磁碟或資料庫中作為 Blob。
13) 在 Java 序列化期間,哪些變數未序列化?
這個問題問得不同, 但目的還是一樣的, Java開發人員是否知道靜態和瞬態變數的細節。由於靜態變數屬於類別, 而不是物件, 因此它們不是物件狀態的一部分, 因此在 Java 序列化過程中不會保存它們。由於 Java 序列化僅保留物件的狀態,而不是物件本身。瞬態變數也不包含在 Java 序列化過程中, 並且不是物件的序列化狀態的一部分。在提出這個問題之後,面試官會詢問後續內容, 如果你不儲存這些變數的值, 那麼一旦對這些物件進行反序列化並重新創建這些變數, 這些變數的價值是多少?這是你們要考慮的。
另一個棘手的核心 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 方法之间的任何潜在竞争条件。
不,你不能在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中重写。
这些是我的核心Java面试问题和答案的清单。对于有经验的程序员来说,一些Java问题看起来并不那么难,但对于Java中的中级和初学者来说,它们真的很难回答。顺便说一句,如果你在面试中遇到任何棘手的Java问题,请与我们分享。
相关推荐:java入门教程
以上是這10個高難度Java面試題你都會麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!