大小廠必問Java後端面試題(含答案)

yes的練級攻略發表於2021-02-20

你好,我是yes。

這個系列的文章不會是背誦版,不是那種貼上標準答案,到時候照著答就行的面試題彙總。

我會用大白話儘量用解釋性、理解性的語言來回答,但是肯定沒有比平時通過一篇文章來講解清晰,不過我儘量。

暫時我先放 20 題出來,字數實在太多了,放一些之後看反饋,然後再修訂,之後再搞個 pdf。

還有,雖說看著只有 20 題,其實不止,因為有些題目我沒拆解,回答中會有延伸問題,所以題目數不止我列出的這些,內容還是非常多的。

剩下還有幾十題,先亮一亮題目,嘿嘿。

文章首發我的個人公眾號:「yes的練級攻略」,之後還有會網路篇、作業系統篇、Spring 篇、Netty 篇等等之類的,感覺滿滿當當的,好充實.....

到時候公眾號後臺也會開放 pdf 提供下載的哈。

好了不多嗶嗶,gogogo!

1.你覺得 Java 好在哪兒?

這種籠統的問題如果對某些知識點沒有深入、系統地認識絕對會蒙!

所以為什麼經常碰到面試官問你一些空、大的問題?其實就是考察你是否有形成體系的理解。

回到問題本身。我覺得可以從跨平臺、垃圾回收、生態三個方面來闡述。

首先 Java 是跨平臺的,不同平臺執行的機器碼是不一樣的,而 Java 因為加了一層中間層 JVM ,所以可以做到一次編寫多平臺執行,即 「Write once,Run anywhere」。

編譯執行過程是先把 Java 原始碼編譯成位元組碼,位元組碼再由 JVM 解釋或 JIT 編譯執行,而因為 JIT 編譯時需要預熱的,所以還提供了 AOT(Ahead-of-Time Compilation),可以直接把位元組碼轉成機器碼,來讓程式重啟之後能迅速拉滿戰鬥力。

(解釋執行比編譯執行效率差,你想想每次給你英語讓你翻譯閱讀,還是直接給你看中文,哪個快?)

Java 還提供垃圾自動回收功能,雖說手動管理記憶體意味著自由、精細化地掌控,但是很容易出錯。

在記憶體較充裕的當下,將記憶體的管理交給 GC 來做,減輕了程式設計師程式設計的負擔,提升了開發效率,更加划算!

然後現在 Java 生態圈太全了,豐富的第三方類庫、網上全面的資料、企業級框架、各種中介軟體等等,總之你要的都有。

基本上這樣答差不多了,之後等著面試官延伸。

當然這種開放性問題沒有固定答案,我的回答僅供參考。

2.如果讓你設計一個 HashMap 如何設計?

這個問題我覺得可以從 HashMap 的一些關鍵點入手,例如 hash函式、如何處理衝突、如何擴容。

可以先說下你對 HashMap 的理解。

比如:HashMap 無非就是一個儲存 <key,value> 格式的集合,用於通過 key 就能快速查詢到 value。

基本原理就是將 key 經過 hash 函式進行雜湊得到雜湊值,然後通過雜湊值對陣列取模找到對應的 index 。

所以 hash 函式很關鍵,不僅運算要快,還需要分佈均勻,減少 hash 碰撞。

而因為輸入值是無限的,而陣列的大小是有限的所以肯定會有碰撞,因此可以採用拉鍊法來處理衝突。

為了避免惡意的 hash 攻擊,當拉鍊超過一定長度之後可以轉為紅黑樹結構。

當然超過一定的結點還是需要擴容的,不然碰撞就太嚴重了。

而普通的擴容會導致某次 put 延時較大,特別是 HashMap 儲存的資料比較多的時候,所以可以考慮和 redis 那樣搞兩個 table 延遲移動,一次可以只移動一部分。

不過這樣記憶體比較吃緊,所以也是看場景來 trade off 了。

不過最好使用之前預估準資料大小,避免頻繁的擴容。

基本上這樣答下來差不多了,HashMap 幾個關鍵要素都包含了,接下來就看面試官怎麼問了。

可能會延伸到執行緒安全之類的問題,反正就照著 currentHashMap 的設計答。

3.併發類庫提供的執行緒池實現有哪些?

雖說阿里巴巴Java 開發手冊禁止使用這些實現來建立執行緒池,但是這問題我被問過好幾次,也是熱點。

問著問著就會延伸到執行緒池是怎麼設計的。

我先來說下執行緒池的內部邏輯,這樣才能理解這幾個實現。

首先執行緒池有幾個關鍵的配置:核心執行緒數、最大執行緒數、空閒存活時間、工作佇列、拒絕策略。

  1. 預設情況下執行緒不會預建立,所以是來任務之後才會建立執行緒(設定prestartAllCoreThreads可以預建立核心執行緒)。
  2. 當核心執行緒滿了之後不會新建執行緒,而是把任務堆積到工作佇列中。
  3. 如果工作佇列放不下了,然後才會新增執行緒,直至達到最大執行緒數。
  4. 如果工作佇列滿了,然後也已經達到最大執行緒數了,這時候來任務會執行拒絕策略。
  5. 如果執行緒空閒時間超過空閒存活時間,並且執行緒執行緒數是大於核心執行緒數的則會銷燬執行緒,直到執行緒數等於核心執行緒數(設定allowCoreThreadTimeOut 可以回收核心執行緒)。

我們再回到面試題來,這個實現指的就是 Executors 的 5 個靜態工廠方法:

  • newFixedThreadPool
  • newWorkStealingPool
  • newSingleThreadExecutor
  • newCachedThreadPool
  • newScheduledThreadPool

newFixedThreadPool

這個執行緒池實現特點是核心執行緒數和最大執行緒數是一致的,然後 keepAliveTime 的時間是 0 ,佇列是無界佇列。

按照這幾個設定可以得知它任務執行緒數是固定,如其名 Fixed。

然後可能出現 OOM 的現象,因為佇列是無界的,所以任務可能擠爆記憶體。

它的特性就是我就固定出這麼多執行緒,多餘的任務就排隊,就算隊伍排爆了我也不管

因此不建議用這個方式來建立執行緒池。

newWorkStealingPool

這個是1.8才有的,從程式碼可以看到返回的就是 ForkJoinPool,我們1.8用的並行流就是這個執行緒池。

比如users.parallelStream().filter(...).sum();用的就是 ForkJoinPool 。

從圖中可以看到執行緒數會參照當前伺服器可用的處理核心數,我記得並行數是核心數-1。

這個執行緒池的特性從名字就可以看出 Stealing,會竊取任務

每個執行緒都有自己的雙端佇列,當自己佇列的任務處理完畢之後,會去別的執行緒的任務佇列尾部拿任務來執行,加快任務的執行速率。

至於 ForkJoin 的話,就是分而治之,把大任務分解成一個個小任務,然後分配執行之後再總和結果,再詳細就自行查閱資料啦~

newSingleThreadExecutor

這個執行緒池很有個性,一個執行緒池就一個執行緒,一個人一座城,配備的也是無界佇列。

它的特性就是能保證任務是按順序執行的

newCachedThreadPool

這個執行緒池是急性子,核心執行緒數是 0 ,最大執行緒數看作無限,然後任務佇列是沒有儲存空間的,簡單理解成來個任務就必須找個執行緒接著,不然就阻塞了。

cached 意思就是會快取之前執行過的執行緒,快取時間是 60 秒,這個時候如果有任務進來就可以用之前的執行緒來執行。

所以它適合用在短時間內有大量短任務的場景。如果暫無可用執行緒,那麼來個任務就會新啟一個執行緒去執行這個任務,快速響應任務

但是如果任務的時間很長,那存在的執行緒就很多,上下文切換就很頻繁,切換的消耗就很明顯,並且存在太多執行緒在記憶體中,也有 OOM 的風險。

newScheduledThreadPool


其實就是定時執行任務,重點就是那個延時佇列。

關於 Java 的幾個定時任務排程相關的:Timer、DelayQueue 和 ScheduledThreadPool,我之前文章都分析過了,還介紹了時間輪在netty和kafka中的應用,有興趣的可以看看。

4.如果讓你設計一個執行緒池如何設計?

這種設計類問題還是一樣,先說下理解,表明你是知道這個東西的用處和原理的,然後開始 BB。基本上就是按照現有的設計來說,再新增一些個人見解。

執行緒池講白了就是儲存執行緒的一個容器,池內儲存之前建立過的執行緒來重複執行任務,減少建立和銷燬執行緒的開銷,提高任務的響應速度,並便於執行緒的管理。

我個人覺得如果要設計一個執行緒池的話得考慮池內工作執行緒的管理、任務編排執行、執行緒池超負荷處理方案、監控。

初始化執行緒數、核心執行緒數、最大執行緒池都暴露出來可配置,包括超過核心執行緒數的執行緒空閒消亡配置。

任務的儲存結構可配置,可以是無界佇列也可以是有界佇列,也可以根據配置分多個佇列來分配不同優先順序的任務,也可以採用 stealing 的機制來提高執行緒的利用率。

再提供配置來表明此執行緒池是 IO 密集還是 CPU 密集型來改變任務的執行策略。

超負荷的方案可以有多種,包括丟棄任務、拒絕任務並丟擲異常、丟棄最舊的任務或自定義等等。

執行緒池埋好點暴露出用於監控的介面,如已處理任務數、待處理任務數、正在執行的執行緒數、拒絕的任務數等等資訊。

我覺得基本上這樣答就差不多了,等著面試官的追問就好。

注意不需要跟面試官解釋什麼叫核心執行緒數之類的,都懂的沒必要。

當然這種開放型問題還是仁者見仁智者見智,我這個不是標準答案,僅供參考。

5. GC 如何調優?

GC 調優這種問題肯定是具體場景具體分析,但是在面試中就不要講太細,大方向說清楚就行,不需要涉及具體的垃圾收集器比如 CMS 調什麼引數,G1 調什麼引數之類的。

GC 調優的核心思路就是儘可能的使物件在年輕代被回收,減少物件進入老年代。

具體調優還是得看場景根據 GC 日誌具體分析,常見的需要關注的指標是 Young GC 和 Full GC 觸發頻率、原因、晉升的速率
、老年代記憶體佔用量等等。

比如發現頻繁會產生 Full GC,分析日誌之後發現沒有記憶體洩漏,只是 Young GC 之後會有大量的物件進入老年代,然後最終觸發 Ful GC。所以就能得知是 Survivor 空間設定太小,導致物件過早進入老年代,因此調大 Survivor 。

或者是晉升年齡設定的太小,也有可能分析日誌之後發現是記憶體洩漏、或者有第三方類庫呼叫了 System.gc等等。

反正具體場景具體分析,核心思想就是儘量在新生代把物件給回收了。

基本上這樣答就行了,然後就等著面試官延伸了。

6.動態代理是什麼?

動態代理就是一個代理機制,動態是相對於靜態來說的。

代理可以看作是呼叫目標的一個包裝,通常用來在呼叫真實的目標之前進行一些邏輯處理,消除一些重複的程式碼。

靜態代理指的是我們預先編碼好一個代理類,而動態代理指的是執行時生成代理類。

動態更加方便,可以指定一系列目標來動態生成代理類(AOP),而不像靜態代理需要為每個目標類寫對應的代理類。

代理也是一種解耦,目標類和呼叫者之間的解耦,因為多了代理類這一層。

常見的動態代理有 JDK 動態代理 和 CGLIB。

7.JDK 動態代理與 CGLIB 區別?

JDK 動態代理是基於介面的,所以要求代理類一定是有定義介面的

CGLIB 基於ASM位元組碼生成工具,它是通過繼承的方式來實現代理類,所以要注意 final 方法

之間的效能隨著 JDK 版本的不同而不同,以下內容取自:haiq的部落格

  • jdk6 下,在執行次數較少的情況下,jdk動態代理與 cglib 差距不明顯,甚至更快一些;而當呼叫次數增加之後, cglib 表現稍微更快一些
  • jdk7 下,情況發生了逆轉!在執行次數較少(1,000,000)的情況下,jdk動態代理比 cglib 快了差不多30%;而當呼叫次數增加之後(50,000,000), 動態代理比 cglib 快了接近1倍
  • jdk8 表現和 jdk7 基本一致

基本上這樣答差不多了,我們再看看 JDK 動態代理實現原理:

  1. 首先通過實現 InvocationHandler 介面得到一個切面類。
  2. 然後利用 Proxy 根據目標類的類載入器、介面和切面類得到一個代理類。
  3. 代理類的邏輯就是把所有介面方法的呼叫轉發到切面類的 invoke() 方法上,然後根據反射呼叫目標類的方法。

再深一點點就是代理類會現在靜態塊中通過反射把所有方法都拿到存在靜態變數中,我之前反編譯看過代理類,我忙寫了一下,大致長這樣:

這一套下來 JDK 動態代理原理應該就很清晰了。

再來看下 CGLIB,其實和 JDK 動態代理的實現邏輯是一致,只是實現方式不同。

        Enhancer en = new Enhancer();
        //2.設定父類,也就是代理目標類,上面提到了它是通過生成子類的方式
        en.setSuperclass(target.getClass());
        //3.設定回撥函式,這個this其實就是代理邏輯實現類,也就是切面,可以理解為JDK 動態代理的handler
        en.setCallback(this);
        //4.建立代理物件,也就是目標類的子類了。
        return en.create();

然後它是通過位元組碼生成技術而不是反射來實現呼叫的邏輯,具體就不再深入了。

8.註解是什麼原理?

註解其實就是一個標記,可以標記在類上、方法上、屬性上等,標記自身也可以設定一些值。

有了標記之後,我們就可以在解析的時候得到這個標記,然後做一些特別的處理,這就是註解的用處。

比如我們可以定義一些切面,在執行一些方法的時候看下方法上是否有某個註解標記,如果是的話可以執行一些特殊邏輯(RUNTIME型別的註解)。

註解生命週期有三大類,分別是:

  • RetentionPolicy.SOURCE:給編譯器用的,不會寫入 class 檔案
  • RetentionPolicy.CLASS:會寫入 class 檔案,在類載入階段丟棄,也就是執行的時候就沒這個資訊了
  • RetentionPolicy.RUNTIME:會寫入 class 檔案,永久儲存,可以通過反射獲取註解資訊

所以我上文寫的是解析的時候,沒寫具體是解析啥,因為不同的生命週期的解析動作是不同的。

像常見的:

就是給編譯器用的,編譯器編譯的時候檢查沒問題就over了,class檔案裡面不會有 Override 這個標記。

再比如 Spring 常見的 Autowired ,就是 RUNTIME 的,所以在執行的時候可以通過反射得到註解的資訊,還能拿到標記的值 required 。

所以註解就是一個標記,可以給編譯器用、也能執行時候用。

9.反射用過嗎?

如果你用過那就不用我多說啥了,場景說一下,然後等著面試官繼續挖。

如果沒用過那就說生產上沒用過,不過私下研究過反射的原理。

反射其實就是Java提供的能在執行期可以得到物件資訊的能力,包括屬性、方法、註解等,也可以呼叫其方法。

一般的編碼不會用到反射,在框架上用的較多,因為很多場景需要很靈活,所以不確定目標物件的型別,屆時只能通過反射動態獲取物件資訊。

PS:對反射不瞭解的,可以網上查查,這裡不深入了。

10.能說下類載入過程嗎?

類載入顧名思義就是把類載入到 JVM 中,而輸入一段二進位制流到記憶體,之後經過一番解析、處理轉化成可用的 class 類,這就是類載入要做的事情。

二進位制流可以來源於 class 檔案,或者通過位元組碼工具生成的位元組碼或者來自於網路都行,只要符合格式的二進位制流,JVM 來者不拒。

類載入流程分為載入、連線、初始化三個階段,連線還能拆分為:驗證、準備、解析三個階段。

所以總的來看可以分為 5 個階段:

  • 載入:將二進位制流搞到記憶體中來,生成一個 Class 類。

  • 驗證:主要是驗證載入進來的二進位制流是否符合一定格式,是否規範,是否符合當前 JVM 版本等等之類的驗證。

  • 準備:為靜態變數(類變數)賦初始值,也即為它們在方法區劃分記憶體空間。這裡注意是靜態變數,並且是初始值,比如 int 的初始值是 0。

  • 解析:將常量池的符號引用轉化成直接引用。符號引用可以理解為只是個替代的標籤,比如你此時要做一個計劃,暫時還沒有人選,你設定了個 A 去做這個事。然後等計劃真的要落地的時候肯定要找到確定的人選,到時候就是小明去做一件事。

    解析就是把 A(符號引用) 替換成小明(直接引用)。符號引用就是一個字面量,沒有什麼實質性的意義,只是一個代表。直接引用指的是一個真實引用,在記憶體中可以通過這個引用查詢到目標。

  • 初始化:這時候就執行一些靜態程式碼塊,為靜態變數賦值,這裡的賦值才是程式碼裡面的賦值,準備階段只是設定初始值佔個坑。

這個問題我覺得回答可以比我寫的更粗,幾個階段一說,大致做的說一說就 ok 了。

想要知道更詳細的流程可以看下《深入理解虛擬機器Java》虛擬機器的類載入章節。

11.雙親委派知道不?來說說看?

類載入機制一問基本上就會接著問雙親委派。

雙親委派的意思是

如果一個類載入器需要載入類,那麼首先它會把這個類載入請求委派給父類載入器去完成,如果父類還有父類則接著委託,每一層都是如此。

一直遞迴到頂層,當父載入器無法完成這個請求時,子類才會嘗試去載入。

這裡的雙親其實就指的是父類,沒有mother。

父類也不是我們平日所說的那種繼承關係,只是呼叫邏輯是這樣。

關於雙親委派我之前寫過文章,我把一些比較重要的內容拷過來:

Java 自身提供了 3 種類載入器:

  1. 啟動類載入器(Bootstrap ClassLoader),它是屬於虛擬機器自身的一部分,用 C++ 實現的,主要負責載入<JAVA_HOME>\lib目錄中或被-Xbootclasspath指定的路徑中的並且檔名是被虛擬機器識別的檔案。它是所有類載入器的爸爸。

  2. 擴充套件類載入器(Extension ClassLoader),它是Java實現的,獨立於虛擬機器,主要負責載入<JAVA_HOME>\lib\ext目錄中或被java.ext.dirs系統變數所指定的路徑的類庫。

  3. 應用程式類載入器(Application ClassLoader),它是Java實現的,獨立於虛擬機器。主要負責載入使用者類路徑(classPath)上的類庫,如果我們沒有實現自定義的類載入器那這玩意就是我們程式中的預設載入器。

所以一般情況類載入會從應用程式類載入器委託給擴充套件類再委託給啟動類,啟動類找不到然後擴充套件類找,擴充套件類載入器找不到再應用程式類載入器找。

雙親委派模型不是一種強制性約束,也就是你不這麼做也不會報錯怎樣的,它是一種JAVA設計者推薦使用類載入器的方式

為什麼要雙親委派?

它使得類有了層次的劃分。就拿 java.lang.Object 來說,載入它經過一層層委託最終是由Bootstrap ClassLoader來載入的,也就是最終都是由Bootstrap ClassLoader去找<JAVA_HOME>\lib中rt.jar裡面的java.lang.Object載入到JVM中。

這樣如果有不法分子自己造了個java.lang.Object,裡面嵌了不好的程式碼,如果我們是按照雙親委派模型來實現的話,最終載入到JVM中的只會是我們rt.jar裡面的東西,也就是這些核心的基礎類程式碼得到了保護。

因為這個機制使得系統中只會出現一個java.lang.Object。不會亂套了。你想想如果我們JVM裡面有兩個Object,那豈不是天下大亂了。

那你知道有違反雙親委派的例子嗎?

典型的例子就是:JDBC。

JDBC 的介面是類庫定義的,但實現是在各大資料庫廠商提供的 jar 包中,那通過啟動類載入器是找不到這個實現類的,所以就需要應用程式載入器去完成這個任務,這就違反了自下而上的委託機制了。

具體做法是搞了個執行緒上下文類載入器,通過 setContextClassLoader() 預設設定了應用程式類載入器,然後通過 Thread.current.currentThread().getContextClassLoader() 獲得類載入器來載入。

12.JDK 和 JRE 的區別?

JRE(Java Runtime Environment)指的是 Java 執行環境,包含了 JVM 和 Java 類庫等。

JDK(Java Development Kit) 可以視為 JRE 的超集,還提供了一些工具比如各種診斷工具:jstack,jmap,jstat 等。

13.用過哪些 JDK 提供的工具?

這個就考察你平日裡面有沒有通過一些工具進行問題的分析、排查。

如果你用過肯定很好說,比如之前排查記憶體異常的時候用 jmap dump下來記憶體檔案用 MAT 進行分析之類的。

如果沒用過的話可以試試,自己找場景試驗一下。

我列幾個之前寫過文章的工具,建議自己用用,還是很簡單的。

  • jps:虛擬機器程式狀況工具
  • jstat:虛擬機器統計資訊監視工具
  • jmap:Java記憶體映像工具
  • jhat:虛擬機器堆轉儲快照分析工具
  • jstack:Java堆疊跟蹤工具
  • jinfo:Java配置資訊工具
  • VisualVM:圖形化工具,可以得到虛擬機器執行時的一些資訊:記憶體分析、CPU 分析等等,在 jdk9 開始不再預設打包進 jdk 中。

工具其實還有很多,看看下面這個截圖。

jdk/bin中部分工具截圖

更詳細的可以去《深入理解虛擬機器Java》第四章檢視。

總之就是自己找機會用用,沒機會就自己給自己創造機會,防範於未然。

14.介面和抽象類有什麼區別?

介面:只能包含抽象方法,不能包含成員變數,當 has a 的情況下用介面。

介面是對行為的抽象,類似於條約。在 Java 中介面可以多實現,從 has a 角度來說介面先行,也就是先約定介面,再實現。

抽象類: 可以包含成員變數和一般方法和抽象方法,當 is a 並且主要用於程式碼複用的場景下使用抽象類繼承的方式,子類必須實現抽象類中的抽象方法。

在 Java 中只支援單繼承。從 is a 角度來看一般都是先寫,然後發現程式碼能複用,然後抽象一個抽象類。

15.什麼是序列化?什麼是反序列化?

序列化其實就是將物件轉化成可傳輸的位元組序列格式,以便於儲存和傳輸。

因為物件在 JVM 中可以認為是“立體”的,會有各種引用,比如在記憶體地址Ox1234 引用了某某物件,那此時這個物件要傳輸到網路的另一端時候就需要把這些引用“壓扁”。

因為網路的另一端的記憶體地址 Ox1234 可以沒有某某物件,所以傳輸的物件需要包含這些資訊,然後接收端將這些扁平的資訊再反序列化得到物件。

所以反序列化就是將位元組序列格式轉換成物件的過程

我再擴充套件一下 Java 序列化。

首先說一下 Serializable,這個介面沒有什麼實際的含義,就是起標記作用。

來看下原始碼就很清楚了,除了 String、陣列和列舉之外,如果實現了這個介面就走writeOrdinaryObject,否則就序列化就拋錯。

serialVersionUID 又有什麼用?

private static final long serialVersionUID = 1L;

想必經常會看到這樣的程式碼,這個 ID 其實就是用來驗證序列化的物件和反序列化對應的物件ID 是否一致。

所以這個 ID 的數字其實不重要,無論是 1L 還是 idea 自動生成的,只要序列化時候物件的 serialVersionUID 和反序列化時候物件的 serialVersionUID 一致的話就行。

如果沒有顯示指定 serialVersionUID ,則編譯器會根據類的相關資訊自動生成一個,可以認為是一個指紋。

所以如果你沒有定義一個 serialVersionUID 然後序列化一個物件之後,在反序列化之前把物件的類的結構改了,比如增加了一個成員變數,則此時的反序列化會失敗。

因為類的結構變了,生成的指紋就變了,所以 serialVersionUID 就不一致了。

所以 serialVersionUID 就是起驗證作用。

Java 序列化不包含靜態變數

簡單地說就是序列化之後儲存的內容不包含靜態變數的值,看下下面的程式碼就很清晰了。

16.什麼是不可變類?

不可變類指的是無法修改物件的值,比如 String 就是典型的不可變類,當你建立一個 String 物件之後,這個物件就無法被修改。

因為無法被修改,所以像執行s += "a"; 這樣的方法,其實返回的是一個新建的 String 物件,老的 s 指向的物件不會發生變化,只是 s 的引用指向了新的物件而已。

所以才會有不要在字串拼接頻繁的場景不要使用 + 來拼接,因為這樣會頻繁的建立物件。

不可變類的好處就是安全,因為知曉這個物件不可能會被修改,因此可以放心大膽的用,在多執行緒環境下也是執行緒安全的。

如何實現一個不可變類?

這個問題我被面試官問過,其實就參考 String 的設計就行。

String 類用 final 修飾,表示無法被繼承。

String 本質是一個 char 陣列,然後用 final 修飾,不過 final 限制不了陣列內部的資料,所以這還不夠。

所以 value 是用 private 修飾的,並且沒有暴露出 set 方法,這樣外部其實就接觸不到 value 所以無法修改。

當然還是有修改的需求,比如 replace 方法,所以這時候就需要返回一個新物件來作為結果。

總結一下就是私有化變數,然後不要暴露 set 方法,即使有修改的需求也是返回一個新物件。

17.Java 按值傳遞還是按引用傳遞?

Java 只有按值傳遞,不論是基本型別還是引用型別。

基本型別是值傳遞很好理解,引用型別有些同學可能有點理解不了,特別是初學者。

JVM 記憶體有劃分為棧和堆,區域性變數和方法引數是在棧上分配的,基本型別和引用型別都佔 4 個位元組,當然 long 和 double 佔 8 個位元組。

而物件所佔的空間是在堆中開闢的,引用型別的變數儲存物件在堆中地址來訪問物件,所以傳遞的時候可以理解為把變數儲存的地址給傳遞過去,因此引用型別也是值傳遞。

18.泛型有什麼用?泛型擦除是什麼?

泛型可以把型別當作引數一樣傳遞,使得像一些集合類可以明確儲存的物件型別,不用顯示地強制轉化(在沒泛型之前只能是Object,然後強轉)。

並且在編譯期能識別型別,型別錯誤則會提醒,增加程式的健壯性和可讀性。

泛型擦除指的指引數型別其實在編譯之後就被抹去了,也就是生成的 class 檔案是沒有泛型資訊的,所以稱之為擦除。

不過這個擦除有個細節,我們來看下程式碼就很清晰了,程式碼如下:

然後我們再來看看編譯後的 class 檔案。

可以看到 yess 是有型別資訊的,所以在程式碼裡寫死的泛型型別是不會被擦除的!

這也解釋了為什麼根據反射是可以拿到泛型資訊的,因為這種寫死的就沒有被擦除!

至於泛型擦除是為了向後相容,因為在 JDK 5 之前是沒有泛型的,所以要保證 JDK 5 之前編譯的程式碼可以在之後的版本上跑,而型別擦除就是能達到這一目標的一個實現手段。

其實 Java 也可以搞別的手段來實現泛型相容,只是擦除比較容易實現。

19.說說強、軟、弱、虛引用?

Java 根據其生命週期的長短將引用型別又分為強引用、軟引用、弱引用、幻象引用。

  • 強引用:就是我們平時 new 一個物件的引用。當 JVM 的記憶體空間不足時,寧願丟擲 OutOfMemoryError 使得程式異常終止,也不願意回收具有強引用的存活著的物件。
  • 軟引用:生命週期比強引用短,當 JVM 認為記憶體空間不足時,會試圖回收軟引用指向的物件,也就是說在 JVM 丟擲 OutOfMemoryError 之前,會去清理軟引用物件,適合用在記憶體敏感的場景。
  • 弱引用:比軟引用還短,在 GC 的時候,不管記憶體空間足不足都會回收這個物件,ThreadLocal中的 key 就用到了弱引用,適合用在記憶體敏感的場景。
    -虛引用:也稱幻象引用,之所以這樣叫是因為虛引用的 get 永遠都是 null ,稱為get 了個寂寞,所以叫虛。

虛引用的唯一作用就是配合引用佇列來監控引用的物件是否被加入到引用佇列中,也就是可以準確的讓我們知曉物件何時被回收。

還有一點有關虛引用的需要提一下,之前看文章都說虛引用對 gc 回收不會有任何的影響,但是看 1.8 doc 上面說

簡單翻譯下就是:與軟引用和弱引用不同,虛引用在排隊時不會被垃圾回收器自動清除。通過虛引用可訪問的物件將保持這種狀態,直到所有這些引用被清除或者它們本身變得不可訪問

簡單的說就是被虛引用引用的物件不能被 gc,然而在 JDK9 又有個變更記錄:

連結:https://bugs.openjdk.java.net/browse/JDK-8071507

按照這上面說的 JDK9 之前虛引用的物件是在虛引用自身被銷燬之前是無法被 gc 的,而 JDK9 之後改了。

我沒下 JDK9 ,不過我有 JDK11 ,所以看了下 11 doc 的確實改了。

看起來是把那段刪了。所以 JDK9 之前虛引用對引用物件的GC是有影響的,9及之後的版本沒影響。

20.Integer 快取池知道嗎?

因為根據實踐發現大部分的資料操作都集中在值比較小的範圍,因此 Integer 搞了個快取池,預設範圍是 -128 到 127,可以根據通過設定JVM-XX:AutoBoxCacheMax=<size>來修改快取的最大值,最小值改不了。

實現的原理是int 在自動裝箱的時候會呼叫Integer.valueOf,進而用到了 IntegerCache。

沒什麼花頭,就是判斷下值是否在範圍之內,如果是的話去 IntegerCache 中取。

IntegerCache 在靜態塊中會初始化好快取值。

所以這裡還有個面試題,就是啥 Integer 127 之內的相等,而超過 127 的就不等了,因為 127 之內的就是同一個物件,所以當然相等。

不僅 Integer 有,Long 也是有的,不過範圍是寫死的 -128 到 127。

對了 Float 和 Double 是沒有滴,畢竟是小數,能存的數太多了。

最後

個人能力有限,如有錯誤歡迎指正~

更多內容,歡迎關注我的公眾號【yes的練級攻略】,每週保證至少分享一篇原創技術文。

相關文章