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


你好,我是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的練級攻略】,每周保證至少分享一篇原創技術文。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM