的基礎(chǔ)題復(fù)盤)
你剛走出面試間手心還殘留著握筆的汗意。那些基礎(chǔ)題像回馬槍一樣殺回來(lái)——明明看過(guò)無(wú)數(shù)遍卻在脫口而出的瞬間卡殼或者自信滿滿地給出一個(gè)標(biāo)準(zhǔn)化答案卻被面試官追問(wèn)得啞口無(wú)言。別急著怪自己臨場(chǎng)發(fā)揮失?;A(chǔ)題的“坑”從來(lái)不在于知識(shí)本身而在于你對(duì)其“底層原理”的理解停留在背誦層面。這次復(fù)盤我們不聊高并發(fā)、不聊分布式就回到Java最樸素的語(yǔ)法和JVM基礎(chǔ)看看那些最容易答錯(cuò)、也最暴露真實(shí)水平的問(wèn)題。與equals你確定你懂“引用比較”嗎幾乎每個(gè)面試官都愛(ài)問(wèn)“和equals的區(qū)別”但鮮少有人能撐過(guò)三輪追問(wèn)。標(biāo)準(zhǔn)答案誰(shuí)都會(huì)背比較地址equals比較內(nèi)容。可一旦換成“String s1 new String(abc); String s2 abc; s1s2的結(jié)果”很多人就開(kāi)始混亂。真正的分水嶺在于有沒(méi)有理解字符串常量池與堆內(nèi)存的分配機(jī)制。new出來(lái)的對(duì)象在堆里字面量指向常量池兩者地址必然不同。但若繼續(xù)追問(wèn)“為什么很多公司要求用String.equals而不是”你就得說(shuō)清楚String重寫了equals的邏輯——先比較引用再比較類型最后逐字符比較數(shù)組。更隱蔽的錯(cuò)誤來(lái)自基本類型包裝類。Integer a 127; Integer b 127; ab返回true但換成128就變成false。緩存機(jī)制的范圍是-128到127超出這個(gè)區(qū)間每次都會(huì)new新對(duì)象。這題答錯(cuò)的人往往會(huì)把“自動(dòng)裝箱”和“緩存”混為一談。面試官真正想聽(tīng)的是你知道Integer內(nèi)部有一個(gè)IntegerCache靜態(tài)類它預(yù)緩存了常用數(shù)值而這是JVM性能優(yōu)化的一種手段。下次再遇到這題別光說(shuō)結(jié)果要把緩存范圍、觸發(fā)條件、為什么設(shè)計(jì)這個(gè)范圍講透——基礎(chǔ)題的高分答案永遠(yuǎn)是“原理場(chǎng)景設(shè)計(jì)動(dòng)機(jī)”三位一體。String、StringBuilder、StringBuffer不可變性的連鎖考題“String為什么設(shè)計(jì)成不可變”這題答“因?yàn)閒inal修飾”只能得20分。面試官想聽(tīng)的是三個(gè)層次第一不可變性帶來(lái)線程安全無(wú)需同步第二字符串常量池可以復(fù)用降低內(nèi)存開(kāi)銷第三hashCode可以緩存適合作為HashMap的key。但最致命的追問(wèn)是“那StringBuilder可變?yōu)槭裁床皇蔷€程安全的它和StringBuffer的區(qū)別僅僅在synchronized嗎”很多人答到“StringBuffer加了同步鎖”就停住了卻忽略了StringBuilder在單線程下性能優(yōu)于StringBuffer是因?yàn)闆](méi)有鎖競(jìng)爭(zhēng)的開(kāi)銷而鎖不僅僅是方法級(jí)別還可能涉及偏向鎖、輕量級(jí)鎖的升級(jí)過(guò)程。更刁鉆的問(wèn)題是“字符串拼接用到底做了什么”。在JDK8及以前編輯器會(huì)把“abc”優(yōu)化成new StringBuilder().append(a).append(b).append(c).toString()所以循環(huán)內(nèi)直接寫“str item”會(huì)不斷創(chuàng)建StringBuilder和String對(duì)象導(dǎo)致OOM風(fēng)險(xiǎn)。而在JDK9之后引入了invokedynamic和StringConcatFactory運(yùn)行期才能真正優(yōu)化。這題答錯(cuò)的人往往還停留在“就是語(yǔ)法糖”的膚淺理解上沒(méi)意識(shí)到編譯優(yōu)化和運(yùn)行期優(yōu)化的區(qū)別。面試官借此判斷你是否關(guān)心版本演進(jìn)是否讀過(guò)JEP相關(guān)文檔。HashMap從存儲(chǔ)結(jié)構(gòu)到擴(kuò)容機(jī)制的連環(huán)陷阱HashMap是面試?yán)锏某G鄻?shù)但基礎(chǔ)題陷阱密集。最經(jīng)典的是“HashMap線程安全嗎為什么不安全”標(biāo)準(zhǔn)答案是“JDK7頭插法會(huì)形成環(huán)JDK8尾插法可能數(shù)據(jù)覆蓋”。但如果你把“put時(shí)modCount不是原子操作”和“resize時(shí)多個(gè)線程同時(shí)rehash導(dǎo)致數(shù)據(jù)丟失”也一并說(shuō)出分?jǐn)?shù)立刻拉開(kāi)。安全問(wèn)題的本質(zhì)是復(fù)合操作的非原子性而不是單步操作的問(wèn)題。再考一題“HashMap在JDK8中為什么要先比較hash再比較equals”很多人背過(guò)“先hash后equals”卻說(shuō)不清原因。因?yàn)閔ashCode定位桶同一個(gè)桶內(nèi)可能有多個(gè)鍵值對(duì)先用hash篩選可以減少equals調(diào)用次數(shù)——當(dāng)hash不同時(shí)對(duì)象一定不相等但hash相同時(shí)對(duì)象不一定相等哈希沖突。所以equals相等的兩個(gè)對(duì)象必須有相同的hashCode但hashCode相同的對(duì)象equals不一定相等。這是Java規(guī)范硬性要求也是HashMap正確運(yùn)作的前提。你若答不出這層邏輯面試官會(huì)懷疑你連對(duì)象比較的基本契約都沒(méi)掌握。接著問(wèn)“HashMap的擴(kuò)容為什么是2的冪次方”多數(shù)人答“為了用位運(yùn)算取?!?。但更深的細(xì)節(jié)是“當(dāng)舊容量是16元素在新數(shù)組的下標(biāo)要么在原位置要么在原位置16這個(gè)規(guī)律怎么來(lái)的”這涉及rehash時(shí)對(duì)hash值高位參與運(yùn)算的理解。JDK8的擴(kuò)容不需要重新計(jì)算hash只需看原h(huán)ash值新增的那一位是0還是1是0則下標(biāo)不變是1則下標(biāo)加舊容量。這個(gè)設(shè)計(jì)精妙且高效而你沒(méi)讀過(guò)源碼根本答不出來(lái)?;A(chǔ)題復(fù)盤的意義就在于此考察的不是你背過(guò)多少結(jié)論而是你能否還原源碼中的每一步設(shè)計(jì)決策。線程與鎖synchronized和volatile的認(rèn)知邊界“volatile能保證原子性嗎”這恐怕是基礎(chǔ)錯(cuò)題里的重災(zāi)區(qū)。上來(lái)就答“volatile保證可見(jiàn)性和有序性不保證原子性”的人會(huì)被追問(wèn)“那i用volatile修飾會(huì)怎樣”正確回答是“i分三步讀-改-寫volatile無(wú)法保證三步連續(xù)執(zhí)行所以結(jié)果可能小于期望值”。但如果你繼續(xù)深入“volatile是如何禁止重排序的”就需要提到內(nèi)存屏障——在每個(gè)volatile寫操作前插入StoreStore屏障在寫后插入StoreLoad屏障讀操作后插入LoadLoad和LoadStore屏障。能報(bào)出這四種屏障名稱和插入位置的人鳳毛麟角。再看synchronized。面試官常問(wèn)“synchronized鎖的是什么”普通方法鎖this靜態(tài)方法鎖Class對(duì)象代碼塊鎖指定對(duì)象。但更進(jìn)階的問(wèn)題是“為什么JDK6要引入偏向鎖和輕量級(jí)鎖”你要從鎖競(jìng)爭(zhēng)的代價(jià)說(shuō)起無(wú)競(jìng)爭(zhēng)時(shí)直接CAS嘗試獲取輕量級(jí)鎖只有競(jìng)爭(zhēng)激烈才升級(jí)為重量級(jí)鎖這是為了減少用戶態(tài)到內(nèi)核態(tài)的切換開(kāi)銷。鎖升級(jí)路徑無(wú)鎖→偏向鎖→輕量級(jí)鎖→重量級(jí)鎖是分析synchronized性能的關(guān)鍵。很多人把“鎖消除”和“鎖粗化”搞混——鎖消除是JIT檢測(cè)到不存在競(jìng)爭(zhēng)時(shí)直接去掉鎖鎖粗化是把多個(gè)相鄰的鎖請(qǐng)求合并成大鎖塊。這兩個(gè)概念雖然不屬于基礎(chǔ)語(yǔ)法但面試官很愛(ài)挖坑因?yàn)樗鼈冊(cè)凇渡钊肜斫釰ava虛擬機(jī)》里就有。異常體系checked與unchecked的哲學(xué)之爭(zhēng)“Java里什么異??梢圆挥貌东@”答案是RuntimeException及其子類以及Error。但很多人忽略了一個(gè)關(guān)鍵點(diǎn)Error也屬于unchecked但異常處理機(jī)制對(duì)待Error的態(tài)度是“程序無(wú)法恢復(fù)不應(yīng)該捕獲”。面試官會(huì)追問(wèn)“自定義異常應(yīng)該繼承Exception還是RuntimeException”這里沒(méi)有絕對(duì)正確的答案但你要說(shuō)出權(quán)衡如果繼承Exception調(diào)用方必須try-catch強(qiáng)制處理如果繼承RuntimeException調(diào)用方可以忽略適合用于非檢查型邏輯錯(cuò)誤。在業(yè)務(wù)開(kāi)發(fā)中80%的異常應(yīng)該設(shè)計(jì)成RuntimeException因?yàn)閺?qiáng)制檢查會(huì)讓方法簽名更臃腫且很多運(yùn)行時(shí)異常根本沒(méi)法在編譯期預(yù)測(cè)。另一個(gè)高頻錯(cuò)點(diǎn)是“finally里return了怎么辦”很多人知道“finally優(yōu)先于catch中的return”但不清楚字節(jié)碼層面的機(jī)制。當(dāng)catch里有returnfinally里有return時(shí)finally的return直接覆蓋掉catch的返回值。更極端的問(wèn)題是“在try里System.exit(0)后finally還會(huì)執(zhí)行嗎”如果面試官說(shuō)“會(huì)”那你就踩坑了。System.exit(0)會(huì)終止當(dāng)前運(yùn)行的JVM無(wú)論后面有沒(méi)有finally都不會(huì)執(zhí)行。涉及SecurityManager時(shí)還要考慮權(quán)限檢查但一般不會(huì)問(wèn)到那么遠(yuǎn)。這個(gè)基礎(chǔ)題背后的邏輯是想考察你對(duì)“程序終止”和“異常退出”語(yǔ)義的區(qū)分——只有退出虛擬機(jī)的調(diào)用才能阻斷finally。反射與代理為什么說(shuō)反射很慢“反射為什么慢說(shuō)說(shuō)你優(yōu)化的思路?!边@題極易答空。籠統(tǒng)講“反射要解析類元數(shù)據(jù)動(dòng)態(tài)調(diào)用”會(huì)顯得單薄。要拆解成三個(gè)層面第一反射調(diào)用方法時(shí)需要檢查方法權(quán)限、入?yún)⒊鰠㈩愋瓦@比直接調(diào)用多了一堆NativeMethodAccessorImpl的本地調(diào)用第二方法調(diào)用要經(jīng)過(guò)Method.invoke的包裝涉及可變參數(shù)裝箱、異常包裝第三JIT無(wú)法對(duì)反射調(diào)用進(jìn)行內(nèi)聯(lián)優(yōu)化。真正能落到實(shí)踐上的優(yōu)化是寫一個(gè)緩存策略把反射獲取的Method、Field緩存起來(lái)避免重復(fù)查找?;蛘吒菀稽c(diǎn)用MethodHandles.Lookup結(jié)合LambdaMetafactory生成調(diào)用點(diǎn)性能接近直接調(diào)用。面試官問(wèn)這題是看你對(duì)“元編程”有沒(méi)有真實(shí)使用經(jīng)驗(yàn)而不是背名詞。還有個(gè)容易被忽視的錯(cuò)點(diǎn)“Class.forName和ClassLoader.loadClass的區(qū)別”。forName會(huì)執(zhí)行靜態(tài)初始化塊即觸發(fā)初始化而loadClass默認(rèn)只做加載、連接不會(huì)初始化。這在寫JDBC驅(qū)動(dòng)時(shí)很關(guān)鍵——Class.forName(com.mysql.jdbc.Driver)注冊(cè)驅(qū)動(dòng)靠的就是靜態(tài)塊。如果你換成ClassLoader.loadClass驅(qū)動(dòng)就沒(méi)注冊(cè)。一句話總結(jié)forName是“加載初始化”loadClass是“惰性加載”。放在基礎(chǔ)題復(fù)盤里這算是“背了API卻不知道副作用”的典型。集合比較Comparable與Comparator的時(shí)機(jī)選擇“一個(gè)類要排序?qū)崿F(xiàn)Comparable好還是用Comparator好”這題看似簡(jiǎn)單但很多人只答“Comparable是自然排序Comparator是自定義排序”沒(méi)有說(shuō)清楚JDK8之后Comparator的lambda語(yǔ)義和鏈?zhǔn)秸{(diào)用。正確姿勢(shì)是如果這個(gè)排序是類的“內(nèi)在屬性”比如Employee按工號(hào)排序就實(shí)現(xiàn)Comparable如果排序邏輯是臨時(shí)的、多變的比如同一批員工今天按年齡排、明天按工資排就用Comparator。更進(jìn)階的是理解Comparator.comparing().thenComparing()的鏈?zhǔn)綄懛ㄒ约皩?duì)于null值處理的nullsFirst/nullsLast。面試官若追“為什么Comparator.compare方法要求o1和o2換位后結(jié)果取反”你要能說(shuō)出“反對(duì)稱性”是排序算法正確性的前提——如果compare(a,b)0且compare(b,a)0那排序器會(huì)陷入混亂。此外TreeSet和TreeMap的排序依賴比較器但如果你把可變對(duì)象放入的話對(duì)象屬性變了卻未更新比較器邏輯會(huì)導(dǎo)致元素丟失。這正是“hashCode和equals影響HashSet而Comparable影響TreeSet”的對(duì)應(yīng)關(guān)系——集合的根數(shù)據(jù)結(jié)構(gòu)決定了它的去重和排序邏輯很多人只記住了HashSet忘了有序集合背后的比較契約。接口與抽象類從語(yǔ)法到設(shè)計(jì)意圖“接口和抽象類怎么選”這題必考答案模板是“語(yǔ)法上接口多實(shí)現(xiàn)抽象類單繼承語(yǔ)義上接口定義能力抽象類定義模板”。但多數(shù)人忽略了Java8之后接口有默認(rèn)方法這打破了“接口只能有抽象方法”的舊印象。面試官也許會(huì)問(wèn)“既然接口可以有default方法那抽象類還有什么存在意義”你要回答抽象類可以保存共享的成員變量、構(gòu)造函數(shù)以及protected方法而接口的字段必須是public static final的默認(rèn)值。更重要的是abstract class可以定義“模板方法”模式讓子類復(fù)用骨架流程而接口的default方法更適合做功能擴(kuò)展和流式API。再深一層面試官可能讓你畫一個(gè)“類實(shí)現(xiàn)兩個(gè)接口它們有相同簽名default方法時(shí)怎么辦”你必須重寫該方法并手動(dòng)指定調(diào)用哪個(gè)接口的default方法。這是語(yǔ)法層面的陷阱但很多人從沒(méi)寫過(guò)這種沖突。如果你能順帶提到“默認(rèn)方法引入的菱形繼承問(wèn)題可以用父類優(yōu)先規(guī)則解決但接口間沖突必須顯式聲明”那就證明你真的思考過(guò)Java多繼承演進(jìn)的邊界。內(nèi)存模型與單例模式雙重檢查鎖為什么需要volatile單例模式幾乎是面試必寫代碼題而雙重檢查鎖DCL中的volatile是問(wèn)得最多的地方。很多人能寫出volatile但說(shuō)不出理由。正確答案instance new Singleton()不是原子操作它拆成三步——分配內(nèi)存、初始化對(duì)象、把引用指向地址。在JIT指令重排序的影響下第三步可能先于第二步執(zhí)行另一個(gè)線程此時(shí)訪問(wèn)到未被初始化的半成品對(duì)象。volatile禁止了這第三步的重排序保證對(duì)象完全構(gòu)造后再暴露引用。這個(gè)考點(diǎn)融合了JMM的happens-before規(guī)則、指令重排序、以及線程間共享變量的可見(jiàn)性是基礎(chǔ)題里含金量極高的一題。更激進(jìn)的問(wèn)法是“有沒(méi)有不用volatile的單例寫法”最優(yōu)雅的是enum單例。因?yàn)镴VM規(guī)范保證了枚舉類型的實(shí)例只能被創(chuàng)建一次且構(gòu)造函數(shù)只能由JVM調(diào)用。枚舉單例不僅天然線程安全還解決了反序列化破壞單例的問(wèn)題——普通類要實(shí)現(xiàn)Serializable就得重寫readResolve而枚舉壓根不需要。如果你能現(xiàn)場(chǎng)演示一個(gè)枚舉單例的獲取方式再對(duì)比懶漢雙重檢查鎖的代碼量面試官眼里會(huì)閃出“這是一個(gè)真正寫過(guò)生產(chǎn)代碼的人”的認(rèn)可?;A(chǔ)扎實(shí)才是高并發(fā)架構(gòu)的底氣走完這十幾道題的復(fù)盤你會(huì)發(fā)現(xiàn)一個(gè)共性每一個(gè)看似簡(jiǎn)單的“基礎(chǔ)題”背后都連接著JVM規(guī)范、源碼實(shí)現(xiàn)和并發(fā)理論。那些在面試中答錯(cuò)的人多半是因?yàn)閷W(xué)習(xí)全靠“八股文”式記憶只記住了答案沒(méi)記住答案從何而來(lái)。而面試官真正在篩選的是那種能從“和equals”一路講到“JMM內(nèi)存屏障”的候選者——因?yàn)橹挥羞@樣的人面對(duì)線上詭異的并發(fā)Bug、性能瓶頸時(shí)才有能力從底層原理出發(fā)推演問(wèn)題而不是依賴百度?;A(chǔ)題不是背誦題而是思維題?;氐阶簧习呀裉齑疱e(cuò)的每一道題沿著“是什么-為什么-源碼怎么實(shí)現(xiàn)-設(shè)計(jì)動(dòng)機(jī)是什么”這條鏈路重新梳理一遍。你不要期待下一次面試碰到原題而要期待每一個(gè)知識(shí)點(diǎn)都能伸出無(wú)數(shù)觸角連成一張網(wǎng)。網(wǎng)越密面試官越難用一句“深入談?wù)劇睋舸┠?。這份復(fù)盤就是你織網(wǎng)的起點(diǎn)。