拆解:從基礎(chǔ)語法到JVM手寫代碼全攻略)
前兩天一個(gè)學(xué)弟甩給我一份文檔標(biāo)題寫著愛奇藝2020校招Java方向筆試題第二場(chǎng)說想讓我?guī)兔潉澲攸c(diǎn)。我翻了翻這套題第一反應(yīng)不是題目有多難而是考察面鋪得很開從Java基礎(chǔ)語法里的運(yùn)算符優(yōu)先級(jí)到面向?qū)ο蟮睦^承多態(tài)再到集合框架、異常處理、JVM內(nèi)存最后還有手寫排序算法。一位同學(xué)在校招季反復(fù)背的java基礎(chǔ)、java面試題、java八股文基本都能在這套卷子里找到對(duì)應(yīng)位置。如果你也正在準(zhǔn)備Java校招或者打算系統(tǒng)過一遍Java核心知識(shí)這篇文章就是沖這類筆試題來的。我會(huì)站在當(dāng)時(shí)考生的視角拆解第二場(chǎng)這類卷子里的高頻考點(diǎn)、常見出題套路和答題邏輯再結(jié)合我這些年看簡(jiǎn)歷、面人的經(jīng)驗(yàn)把真正拉分的細(xì)節(jié)講透尤其是那些看起來像送分題、實(shí)際上最容易丟分的地方。1. 從這套題的命題地圖看Java校招筆試的底層規(guī)律1.1 大廠筆試為什么偏愛基礎(chǔ)八股加手寫代碼的組合很多人看到八股文三個(gè)字就皺眉覺得這是死記硬背。但如果你站在出題人的角度想一場(chǎng)筆試要在有限時(shí)間里篩出基礎(chǔ)扎實(shí)、能寫出正確代碼、能快速定位問題的候選人最有效率的方式就是組合拳選擇題考知識(shí)面的寬度手寫題考代碼落地的能力。愛奇藝2020校招Java方向的第二場(chǎng)筆試題給我的感覺就是非常典型的基礎(chǔ)廣度加代碼深度并重的卷子。它不會(huì)問你做過什么項(xiàng)目這種開放性話題筆試階段也沒法做深度項(xiàng)目考察所以只能把Java語言規(guī)范、JDK核心類庫、JVM基礎(chǔ)、常用算法這些硬通貨拿出來考。這些知識(shí)點(diǎn)有一個(gè)共同特點(diǎn)會(huì)就是會(huì)不會(huì)就是不會(huì)很難蒙。也正因?yàn)槿绱诉@類筆試題對(duì)科班和非科班選手的區(qū)分度極高??瓢嗤瑢W(xué)在大學(xué)課程里接觸過Java但未必系統(tǒng)刷過題培訓(xùn)班出身或者自學(xué)轉(zhuǎn)行的同學(xué)往往在框架層面知道很多反而在語言底層細(xì)節(jié)上容易栽跟頭。我的建議始終是不要因?yàn)轭}目看起來基礎(chǔ)就輕視它越是基礎(chǔ)的地方越要較真。1.2 從第二場(chǎng)的題型分布推斷考察權(quán)重雖然我沒有辦法把當(dāng)年原卷逐字復(fù)述出來但這類校招筆試的題型分布一直很穩(wěn)定基本可以歸納為以下幾塊考察模塊典型出題形式難度復(fù)習(xí)性價(jià)比語法與運(yùn)算符單選、代碼結(jié)果判斷低高面向?qū)ο筇匦詥芜x、多選中高集合框架單選、對(duì)比類題目中高異常處理代碼判斷題、改錯(cuò)中高JVM與內(nèi)存單選、概念判斷中中枚舉與Lambda代碼題、補(bǔ)全代碼中低中排序與算法手寫代碼中高高從這個(gè)表格能看出來真正拉開差距的不是那些偏難怪的知識(shí)點(diǎn)反而是Java基礎(chǔ)里最常見的類庫和語法細(xì)節(jié)。很多同學(xué)一到手寫排序就懵或者對(duì)HashMap的底層原理說不清楚這些才是筆試失分的重災(zāi)區(qū)。所以接下來的篇幅我會(huì)重點(diǎn)講這些高頻模塊的實(shí)際考法和答題思路。2. 編譯報(bào)錯(cuò)、環(huán)境變量與標(biāo)識(shí)符送分題里最容易丟分的細(xì)節(jié)2.1 源發(fā)行版17需要目標(biāo)發(fā)行版17背后的Java版本知識(shí)如果你在IDEA里寫過Java 17的代碼一定見過這樣的提示java: 警告: 源發(fā)行版 17 需要目標(biāo)發(fā)行版 17。筆試不會(huì)直接讓你配環(huán)境但它完全可以把一段Java代碼和一段Maven配置放在一起讓你判斷這段代碼為什么編譯不通過。這個(gè)問題的本質(zhì)其實(shí)是Java編譯器對(duì)source和target兩個(gè)參數(shù)的要求。source代表你寫代碼時(shí)用的語言版本target代表編譯產(chǎn)物所支持的JVM最低版本。當(dāng)你把source設(shè)置成17而target還停留在8或者更低時(shí)編譯器會(huì)認(rèn)為你要求用新版本的語法卻產(chǎn)出一個(gè)舊版本JVM能識(shí)別的字節(jié)碼這在某些語法特性上是做不到的于是就會(huì)給出上面的警告甚至直接報(bào)錯(cuò)。放在筆試?yán)锼鋵?shí)是在考察你對(duì)Java版本演進(jìn)和編譯過程是否有概念。項(xiàng)目里最常見的情況是IDEA的Project Structure里把SDK切到了17但Java Compiler面板里的Target bytecode version還留在8或者M(jìn)aven的maven.compiler.source和maven.compiler.target不一致。解決辦法就是把這兩者對(duì)齊或者干脆在pom.xml里用maven.compiler.release統(tǒng)一指定版本。這類題目真正想測(cè)試的是你有沒有在實(shí)際開發(fā)中遇到過環(huán)境問題并且有沒有能力快速定位根因。很多應(yīng)屆生在簡(jiǎn)歷上寫熟悉Java但從來沒自己配過JAVA_HOME、沒處理過編譯版本沖突遇到這種題就只能靠猜了。2.2 標(biāo)識(shí)符命名規(guī)則與運(yùn)算符優(yōu)先級(jí)更基礎(chǔ)的語言題標(biāo)識(shí)符命名規(guī)則屬于那種一看就會(huì)、一考就錯(cuò)的知識(shí)點(diǎn)。Java規(guī)定標(biāo)識(shí)符由字母、數(shù)字、下劃線_和美元符$組成不能以數(shù)字開頭不能是Java關(guān)鍵字也不能是字面量true、false、null。但有一個(gè)細(xì)節(jié)大家經(jīng)常忽略Java 9開始單獨(dú)的下劃線_已經(jīng)被保留為關(guān)鍵字不能作為標(biāo)識(shí)符使用了。筆試題很喜歡在這種細(xì)節(jié)上做文章。它給你一組選項(xiàng)比如123abc、_name、$value、class、String、int_問哪些是合法標(biāo)識(shí)符。很多人一看String是類名就覺得不能用作變量名但實(shí)際上Java里的類名并不是關(guān)鍵字String s hello完全合法只是不推薦這么寫。同理int才是關(guān)鍵字int_不是。運(yùn)算符優(yōu)先級(jí)這塊就更有意思了。筆試常出這類題int x 5; int y x x; System.out.println(y);如果你算出來的結(jié)果是12說明你理解得很穩(wěn)。過程是這樣的x先用原值5參與運(yùn)算之后x變成6x先把x從6變成7再參與運(yùn)算所以結(jié)果是5加7等于12。這類題目反復(fù)出現(xiàn)的意義不是真的要你寫這種代碼而是考察你對(duì)優(yōu)先級(jí)和自增自減的理解是否清晰。2.3 環(huán)境變量配置與java命令筆試中的隱藏考點(diǎn)環(huán)境變量這塊筆試題一般不會(huì)白紙黑字問你JAVA_HOME怎么配但會(huì)出現(xiàn)下面哪個(gè)命令能正確執(zhí)行一個(gè)帶外部依賴的Java程序這類問題。這時(shí)候java -cp或者java -classpath必須出現(xiàn)在你的知識(shí)儲(chǔ)備里。CLASSPATH的作用是告訴JVM去哪里找類和jar包。如果你在命令行下運(yùn)行一個(gè)需要依賴第三方庫的程序只寫java Main大概率會(huì)報(bào)ClassNotFoundException。正確寫法是java -cp .:lib/commons-lang3.jar Main在Windows下路徑分隔符是分號(hào)Linux和macOS下是冒號(hào)。這個(gè)細(xì)節(jié)也是筆試喜歡挖坑的點(diǎn)選項(xiàng)里把路徑分隔符寫錯(cuò)或者把-cp和-D的位置搞混。JAVA_HOME和PATH的關(guān)系也要拎清楚。JAVA_HOME通常配置成JDK安裝目錄PATH里加入%JAVA_HOME%/bin別問為什么問就是很多工具如Maven、Gradle、Tomcat默認(rèn)會(huì)從JAVA_HOME去定位Java環(huán)境。如果筆試題目描述一個(gè)java -version有輸出但javac找不到的場(chǎng)景你就要立刻想到PATH里可能只配了JRE的bin目錄或者配了多個(gè)版本的JDK發(fā)生了沖突。3. 面向?qū)ο蟆⒚杜e與Lambda八股文背后的設(shè)計(jì)思維考察3.1 面向?qū)ο笕筇匦栽诠P試題中的出題形式面向?qū)ο笫荍ava筆試永遠(yuǎn)繞不開的主題但考的絕不是什么是封裝這種問答題而是代碼層面的理解。比如多態(tài)的經(jīng)典題目class Animal { public void shout() { System.out.println(animal); } } class Dog extends Animal { Override public void shout() { System.out.println(dog); } } Animal a new Dog(); a.shout();輸出結(jié)果是dog這一點(diǎn)大多數(shù)人都知道。但如果把一個(gè)成員變量也設(shè)計(jì)成同名比如父類和子類都有String name再通過父類引用調(diào)用a.name那訪問到的就是父類的name因?yàn)镴ava中成員變量不存在多態(tài)只有方法才存在多態(tài)。這種變量與方法行為不一致的題目命中率特別高。還有重載和重寫的區(qū)別。筆試題會(huì)給你一堆方法簽名讓你判斷哪些能構(gòu)成重載、哪些是重寫、哪些是非法定義。核心判斷標(biāo)準(zhǔn)只有兩條方法名相同且參數(shù)列表不同是重載方法簽名相同且返回類型兼容、訪問權(quán)限不縮小是重寫。別被throws異常、泛型這些次要點(diǎn)干擾。3.2 枚舉類型不只是常量類很多同學(xué)對(duì)枚舉的理解停留在用來定義常量這個(gè)層面其實(shí)Java的枚舉是個(gè)完整類它有構(gòu)造器、字段、方法甚至可以繼承接口。筆試中常見的一個(gè)坑是問枚舉是否可以使用比較答案是可以因?yàn)槊總€(gè)枚舉常量在JVM中只有一個(gè)實(shí)例和equals效果相同。再比如這個(gè)場(chǎng)景定義一個(gè)帶屬性的枚舉。enum Status { SUCCESS(200, 成功), ERROR(500, 失敗); private final int code; private final String msg; Status(int code, String msg) { this.code code; this.msg msg; } public int getCode() { return code; } }這種寫法在很多項(xiàng)目里其實(shí)很常見筆試也喜歡讓你根據(jù)枚舉定義反推構(gòu)造器調(diào)用方式或者判斷values()和valueOf()的用法。values()是編譯器自動(dòng)生成的方法返回所有枚舉常量數(shù)組valueOf(String)可以根據(jù)名字找到對(duì)應(yīng)常量找不到會(huì)拋IllegalArgumentException。還有一個(gè)冷門考點(diǎn)枚舉類型是可以用在switch里的Java會(huì)為你做類似switch on Enum的優(yōu)化直接按枚舉常量跳轉(zhuǎn)效率很高。3.3 Lambda與函數(shù)式接口代碼怎么寫才符合規(guī)范Java 8的Lambda表達(dá)式是面試和筆試的???。要理解Lambda先理解函數(shù)式接口只有一個(gè)抽象方法的接口比如Runnable、Comparator、Consumer。Lambda表達(dá)式本質(zhì)上就是這個(gè)接口的匿名實(shí)現(xiàn)對(duì)象。筆試?yán)镪P(guān)于Lambda的坑主要在兩個(gè)地方。第一個(gè)lambda表達(dá)式里引用的局部變量必須是effectively final也就是變量初始化之后不再被修改。如果你在lambda內(nèi)部嘗試修改外部局部變量的值編譯器會(huì)直接給error。第二個(gè)方法引用的寫法。list.sort(Comparator.comparingInt(String::length))和list.sort((a, b) - a.length() - b.length())表達(dá)的是同一個(gè)意思但前者可讀性更強(qiáng)也顯得你更懂函數(shù)式編程。還有一個(gè)實(shí)用場(chǎng)景經(jīng)常出現(xiàn)在筆試題里把某個(gè)指定元素排到集合最前面。很多人第一時(shí)間想的是遍歷然后remove再add但其實(shí)用Comparator一行就能寫完list.sort(Comparator .comparing((String s) - !target.equals(s)) .thenComparing(Comparator.naturalOrder()));思路是利用boolean排序false自然排在true前面所以target.equals(s)的結(jié)果為true的元素反而會(huì)被排在后面。那我們先讓條件對(duì)目標(biāo)元素為false即!target.equals(s)目標(biāo)元素就排到最前了。這個(gè)寫法很巧妙筆試?yán)锬芙o閱卷人留下不錯(cuò)的印象。3.4 Lombok編譯期警告注解處理器的延伸考點(diǎn)熱搜詞里有一條很經(jīng)典的報(bào)錯(cuò)you arent using a compiler supported by lombok, so lombok will not work with your project。這個(gè)報(bào)錯(cuò)的意思是Lombok注解處理器在當(dāng)前編譯環(huán)境中沒有生效導(dǎo)致它無法在編譯期幫你生成getter、setter、構(gòu)造器等方法。為什么筆試會(huì)關(guān)注這個(gè)因?yàn)長(zhǎng)ombok涉及注解的RetentionPolicy。Java注解的保留策略有SOURCE、CLASS、RUNTIME三種。Lombok的注解之所以能在編譯期自動(dòng)生成代碼靠的正是自定義注解處理器Annotation Processor它作用于編譯階段最終生成的代碼也不會(huì)出現(xiàn)在源碼里。當(dāng)你用IDE運(yùn)行時(shí)需要在Annotation Processing里開啟對(duì)應(yīng)選項(xiàng)當(dāng)你用Maven編譯時(shí)需要確保依賴和插件配置正確。這類題目真正考察的是你不僅會(huì)用Lombok還知道它為什么能這么用。知道了注解處理器和編譯期代碼生成這件事以后遇到其他類似庫比如MapStruct也就不會(huì)覺得神秘了。4. 集合框架與容器從ArrayList到HashMap的追問邏輯4.1 容器類的基本功橫向?qū)Ρ阮}怎么答集合框架在筆試?yán)镎急炔恍〕鲱}風(fēng)格也非常穩(wěn)定要么橫向?qū)Ρ纫瓷钔谠创a。橫向?qū)Ρ茸畛R姷木褪茿rrayList和LinkedList。ArrayList底層是Object數(shù)組查詢快按下標(biāo)訪問時(shí)間復(fù)雜度O(1)中間插入或刪除需要搬運(yùn)元素是O(n)。LinkedList底層是雙向鏈表插入和刪除在頭尾時(shí)是O(1)但隨機(jī)訪問要遍歷也是O(n)。一旦題目加一個(gè)限定條件頻繁在中間插入刪除答案就要傾向LinkedList如果題目強(qiáng)調(diào)隨機(jī)訪問多、內(nèi)存占用小那就選ArrayList。看清楚題目給的應(yīng)用場(chǎng)景再答別背結(jié)論。線程安全容器也是高頻考點(diǎn)。HashMap不是線程安全的Hashtable是線程安全的但它的所有方法都加了synchronized鎖并發(fā)效率很低。ConcurrentHashMap是Java 5之后推薦的并發(fā)容器JDK 8之后底層用數(shù)組加鏈表加紅黑樹鎖粒度從分段鎖優(yōu)化到了單個(gè)數(shù)組桶的synchronized加CAS并發(fā)性能提升非常明顯。筆試如果問哪種容器更適合高并發(fā)場(chǎng)景下的鍵值存儲(chǔ)答案通常是ConcurrentHashMap。4.2 HashMap源碼細(xì)節(jié)為什么它是常青樹HashMap是集合框架里被問得最頻繁的一個(gè)筆試題可以從多個(gè)角度切入。JDK 1.8之后HashMap的底層結(jié)構(gòu)是數(shù)組加鏈表加紅黑樹。put一個(gè)鍵值對(duì)時(shí)先通過鍵的hashCode計(jì)算數(shù)組下標(biāo)真正存儲(chǔ)時(shí)會(huì)先對(duì)key的hash做一次擾動(dòng)處理即h ^ (h 16)目的是讓高16位也參與下標(biāo)計(jì)算降低哈希沖突概率。如果多個(gè)key落在同一個(gè)數(shù)組桶里就以鏈表形式串起來當(dāng)鏈表長(zhǎng)度達(dá)到8并且數(shù)組長(zhǎng)度達(dá)到64時(shí)鏈表會(huì)轉(zhuǎn)換成一棵紅黑樹把查找時(shí)間從O(n)降到O(logn)。這些數(shù)字和條件很容易考但它們背后的邏輯更重要。為什么是8因?yàn)榧t黑樹的節(jié)點(diǎn)對(duì)象比普通鏈表節(jié)點(diǎn)大在哈希函數(shù)足夠分散的前提下桶中鏈表長(zhǎng)度到8的概率很低所以只有在真的出現(xiàn)大量沖突時(shí)才升級(jí)成樹用空間換時(shí)間。為什么數(shù)組長(zhǎng)度要滿64因?yàn)槿绻麛?shù)組還不夠大優(yōu)先擴(kuò)容把元素重新散列可能比轉(zhuǎn)樹更劃算。默認(rèn)容量16和負(fù)載因子0.75也是高頻考點(diǎn)。負(fù)載因子0.75意味著當(dāng)元素?cái)?shù)量達(dá)到容量乘以0.75時(shí)會(huì)觸發(fā)擴(kuò)容擴(kuò)容后容量翻倍所有元素要重新計(jì)算下標(biāo)位置這個(gè)過程叫rehash。0.75是時(shí)間和空間成本之間的一個(gè)折中值太小了容易頻繁擴(kuò)容浪費(fèi)空間太大了雖然省空間但沖突概率升高影響查找效率。這個(gè)為什么是0.75的說明是答案里最體現(xiàn)水平的加分項(xiàng)。4.3 Comparator與Comparable讓排序題更穩(wěn)的底層理解排序是筆試手寫題的家常便飯而懂排序不只是會(huì)寫快排還要會(huì)用Comparable和Comparator去控制排序規(guī)則。Comparable讓一個(gè)類具備天生可比較的能力需要實(shí)現(xiàn)compareTo方法比如Integer、String都是這么做的。Comparator則是外部比較器你可以在不修改類源碼的情況下臨時(shí)定義多種排序規(guī)則。兩者并不沖突使用場(chǎng)景不同如果這個(gè)類本身就應(yīng)該有自然順序用Comparable如果同一類在不同業(yè)務(wù)場(chǎng)景有不同排序方式用Comparator更合適。手寫Comparator時(shí)有個(gè)常見錯(cuò)誤是返回值為a - b當(dāng)a和b非常接近Integer.MAX_VALUE時(shí)可能溢出。筆試?yán)锟梢怨室饨o你一段return o1.age - o2.age;的代碼問你有沒有問題。最佳做法是用Integer.compare(o1.age, o2.age)這看起來是小事但能體現(xiàn)你對(duì)邊界情況的敏感度。還有一條如果想讓排序結(jié)果穩(wěn)定要避免在比較邏輯里返回0以外的隨機(jī)值Java的List.sort是穩(wěn)定排序相等的元素能保持原始相對(duì)順序但這個(gè)前提是你的比較器在相等時(shí)返回0。5. 內(nèi)存溢出、數(shù)組越界與異常體系JVM題目答題框架5.1 OutOfMemoryError和數(shù)組越界到底考什么java.lang.OutOfMemoryError在熱搜詞里也出現(xiàn)過它是很多生產(chǎn)環(huán)境事故的元兇筆試當(dāng)然不會(huì)放過。常見的內(nèi)存溢出場(chǎng)景可以分成幾類Java堆溢出通常是創(chuàng)建了太多對(duì)象且無法回收元空間溢出JDK 8之后的方法區(qū)實(shí)現(xiàn)通常是動(dòng)態(tài)生成類或加載了太多類線程棧溢出表現(xiàn)為StackOverflowError通常由無遞歸結(jié)束條件導(dǎo)致。hotspot VM里有個(gè)經(jīng)典現(xiàn)象是容器環(huán)境下報(bào)insufficient memory它往往不是Java堆不夠而是操作系統(tǒng)層面沒有足夠的內(nèi)存給JVM分配比如容器內(nèi)存限制和JVM啟動(dòng)參數(shù)里的-Xmx不匹配。這道題出現(xiàn)在筆試?yán)锲鋵?shí)是在考察你對(duì)JVM運(yùn)行數(shù)據(jù)區(qū)域的理解堆、虛擬機(jī)棧、本地方法棧、方法區(qū)、程序計(jì)數(shù)器各自存什么誰會(huì)出現(xiàn)哪種異常必須對(duì)號(hào)入座。數(shù)組越界是另一個(gè)高頻異常。它的Java異常類是ArrayIndexOutOfBoundsException屬于RuntimeException也就是傳說中的非受檢異常。筆試如果給一段循環(huán)遍歷數(shù)組的代碼問你為什么拋異常十有八九是循環(huán)邊界寫錯(cuò)。比如for (int i 0; i arr.length; i) { System.out.println(arr[i]); }i等于arr.length時(shí)arr[i]已經(jīng)越界。這類錯(cuò)誤之所以高頻是因?yàn)槊總€(gè)人寫循環(huán)時(shí)都容易差一個(gè)邊界。筆試手寫代碼時(shí)養(yǎng)成用i arr.length的小習(xí)慣能幫你避開很多坑。5.2 手寫異常處理代碼的正確姿勢(shì)異常處理不只要認(rèn)識(shí)異常類還要會(huì)寫正確的處理代碼。筆試常出這樣的代碼題判斷下面代碼是否能編譯通過或者輸出什么。try-with-resources是Java 7引入的語法用來簡(jiǎn)化資源關(guān)閉。筆試經(jīng)常問下面這段代碼執(zhí)行完后被打開的文件流會(huì)自動(dòng)關(guān)閉嗎t(yī)ry (FileInputStream in new FileInputStream(a.txt)) { // 處理 }答案是完全會(huì)而且不需要finally塊。try-with-resources要求資源類實(shí)現(xiàn)AutoCloseable接口這里還有一個(gè)細(xì)節(jié)catch塊和finally塊仍然可以跟在try-with-resources后面且多個(gè)資源會(huì)按照聲明順序的逆序關(guān)閉。catch子句的順序也是個(gè)經(jīng)典考點(diǎn)。多異常catch時(shí)子類異常必須放在父類異常前面。比如同時(shí)catch IOException和Exception如果IOException寫在Exception后面編譯器直接報(bào)錯(cuò)。原因是如果父類在前子類異常永遠(yuǎn)輪不到被捕獲這屬于不可達(dá)代碼。另外try、catch、finally里都有return時(shí)finally中的return會(huì)覆蓋try或catch中的return如果不寫return只修改返回值finally的操作也會(huì)在方法返回前執(zhí)行但具體返回值是finally修改后的值還是修改前的值要區(qū)分基本類型和引用類型這種題很容易讓粗心的人翻車。5.3 筆試題里JVM相關(guān)選擇題的常見陷阱JVM不是一門語言但Java筆試基本都會(huì)帶幾道JVM題。最常考的是內(nèi)存區(qū)域劃分、類加載過程、可達(dá)性分析。類加載過程是加載、驗(yàn)證、準(zhǔn)備、解析、初始化五個(gè)階段。其中準(zhǔn)備階段會(huì)為類的靜態(tài)變量分配內(nèi)存并設(shè)置初始值比如static int a 10在準(zhǔn)備階段a的值是0到初始化階段才賦值為10。這個(gè)兩次賦值的細(xì)節(jié)是筆試選擇題非常愛挖的點(diǎn)。GC相關(guān)的題目則集中在可達(dá)性分析上。哪些對(duì)象可以作為GC Roots常見的有虛擬機(jī)棧中引用的對(duì)象、靜態(tài)變量引用的對(duì)象、本地方法棧中Native方法引用的對(duì)象。只有被GC Roots直接或間接引用的對(duì)象才會(huì)被標(biāo)記為存活其他對(duì)象會(huì)被判定為可回收。這里有個(gè)冷門陷阱兩個(gè)對(duì)象互相循環(huán)引用但它們都不再被GC Roots引用這種情況下兩者都會(huì)被回收。面試中經(jīng)常把這個(gè)問題包裝成引用計(jì)數(shù)法的缺陷筆試同樣可能考到。答題框架其實(shí)很簡(jiǎn)單先把內(nèi)存區(qū)域按線程隔離與否分清楚再把各區(qū)域會(huì)拋什么異常對(duì)應(yīng)上最后能用一句話說清可達(dá)性分析大部分JVM選擇題就能穩(wěn)拿分了。6. 冒泡排序與快速排序手寫代碼題的滿分書寫規(guī)范6.1 冒泡排序的優(yōu)化寫法與復(fù)雜度推導(dǎo)手寫冒泡排序是很多Java筆試題的第一道代碼題因?yàn)樗T檻低但可以玩的花樣不少。基礎(chǔ)寫法很簡(jiǎn)單兩層循環(huán)相鄰比較大的往后冒。但如果你直接按最樸素方式寫只能算及格因?yàn)椴簧匍喚砣诉€想看到優(yōu)化意識(shí)。優(yōu)化手段是加一個(gè)布爾標(biāo)志位。如果某一輪循環(huán)中沒有任何元素交換說明整個(gè)數(shù)組已經(jīng)有序可以提前終止。public static void bubbleSort(int[] arr) { if (arr null || arr.length 2) { return; } for (int i 0; i arr.length - 1; i) { boolean swapped false; for (int j 0; j arr.length - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; swapped true; } } if (!swapped) { break; } } }為什么最好情況時(shí)間復(fù)雜度是O(n)因?yàn)楫?dāng)數(shù)組已經(jīng)是有序的時(shí)候第一輪掃描一個(gè)元素都沒交換循環(huán)直接退出掃描了n個(gè)元素所以是O(n)。最壞和平均都是O(n^2)。冒泡排序是穩(wěn)定排序因?yàn)橄嗟鹊脑夭粫?huì)交換位置這是它和選擇排序的重要區(qū)別。筆試如果追問穩(wěn)定性的含義和應(yīng)用場(chǎng)景可以說是穩(wěn)定排序的算法在按多個(gè)字段排序時(shí)不會(huì)破壞前一個(gè)字段已經(jīng)排好的相對(duì)順序。6.2 快速排序?qū)崿F(xiàn)時(shí)最容易被扣分的三個(gè)點(diǎn)快速排序是筆試高頻手寫題難度比冒泡高不少。最常見的寫法是用Lomuto分區(qū)方案選取最右邊的元素作為pivot然后遍歷分區(qū)。public static void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivotIndex partition(arr, left, right); quickSort(arr, left, pivotIndex - 1); quickSort(arr, pivotIndex 1, right); } private static int partition(int[] arr, int left, int right) { int pivot arr[right]; int i left; for (int j left; j right; j) { if (arr[j] pivot) { swap(arr, i, j); i; } } swap(arr, i, right); return i; } private static void swap(int[] arr, int i, int j) { int tmp arr[i]; arr[i] arr[j]; arr[j] tmp; }這個(gè)寫法能跑通但容易在三個(gè)地方被扣分。第一遞歸終止條件只寫一個(gè)left right不夠還要考慮數(shù)組為空或者長(zhǎng)度為1的邊界場(chǎng)景。上面的代碼里quickSort函數(shù)入口雖然沒有顯式判斷arr是否為null但調(diào)用方如果傳入nullpartition里直接取arr[right]會(huì)空指針。筆試閱卷人很看重邊界處理建議在quickSort里加一個(gè)if (arr null || arr.length 2) return;的守衛(wèi)。第二分區(qū)循環(huán)里用的是if (arr[j] pivot)而不是。如果寫成等于pivot的元素會(huì)頻繁交換雖然不是致命錯(cuò)誤但會(huì)讓人懷疑你沒理解分區(qū)邏輯的語義。第三遞歸深度。當(dāng)輸入數(shù)組已經(jīng)有序且每次都選最后一個(gè)元素當(dāng)pivot時(shí)快速排序會(huì)退化到O(n^2)而且遞歸深度可能接近數(shù)組長(zhǎng)度導(dǎo)致棧溢出。筆試的時(shí)候如果時(shí)間允許可以補(bǔ)一句在工程實(shí)現(xiàn)上可以隨機(jī)選擇pivot或三數(shù)取中避免退化這屬于加分表述能讓閱卷人看出你學(xué)過而不僅僅是背過。平均時(shí)間復(fù)雜度是O(nlogn)最壞是O(n^2)空間復(fù)雜度因?yàn)檫f歸調(diào)用是O(logn)。快速排序是不穩(wěn)定排序這一點(diǎn)也常被拿來出選擇題。6.3 筆試手寫代碼的時(shí)間分配與檢查順序手寫代碼題在有限時(shí)間內(nèi)最容易犯的錯(cuò)反而不是算法不會(huì)而是低級(jí)錯(cuò)誤。我見過太多次思路完全正確但因?yàn)檠h(huán)邊界少寫一個(gè)等于號(hào)導(dǎo)致數(shù)組越界判卷直接扣大分的案例。我的建議是先花30秒讀清楚題目要求確認(rèn)輸入輸出格式再動(dòng)手寫主干邏輯最后補(bǔ)邊界條件寫完后用一兩個(gè)簡(jiǎn)單用例在腦子里走一遍。檢查順序可以是數(shù)組是否可能為null、長(zhǎng)度為0、長(zhǎng)度為1循環(huán)邊界是否可能出現(xiàn)等于length的情況遞歸終止條件是否一定被觸發(fā)swap操作是否因?yàn)閮蓚€(gè)下標(biāo)相同而出問題。這些檢查點(diǎn)看起來瑣碎但真能幫你挽回不少分。筆試和日常開發(fā)不一樣日常有編譯器幫你兜底筆試全是裸寫。代碼的可讀性也很重要變量名別用a、b、c敷衍用arr、left、right、pivotIndex這種能看懂的名字閱卷印象分完全不同。7. 針對(duì)第二場(chǎng)這類筆試的復(fù)習(xí)路線與臨場(chǎng)節(jié)奏7.1 一份可執(zhí)行的Java基礎(chǔ)復(fù)習(xí)清單如果你準(zhǔn)備的時(shí)間還比較充裕建議按照下面這個(gè)順序過知識(shí)點(diǎn)這套順序基本對(duì)應(yīng)試卷的難度遞增Java語法標(biāo)識(shí)符、基本數(shù)據(jù)類型、運(yùn)算符、流程控制、數(shù)組。面向?qū)ο箢?、?duì)象、封裝、繼承、多態(tài)、接口、抽象類、重載、重寫。常用類庫String、StringBuilder、包裝類、枚舉、日期時(shí)間API。集合框架List、Set、Map的實(shí)現(xiàn)類與底層結(jié)構(gòu)重點(diǎn)啃HashMap。異常體系Throwable、Error、Exception、RuntimeException、try-with-resources。JVM基礎(chǔ)運(yùn)行時(shí)數(shù)據(jù)區(qū)域、類加載過程、GC、內(nèi)存溢出場(chǎng)景。Java 8特性Lambda、Stream、函數(shù)式接口、方法引用、Optional。手寫算法冒泡、快排、二分查找、單鏈表反轉(zhuǎn)、括號(hào)匹配。每一步都配點(diǎn)頭量練習(xí)。牛客網(wǎng)的Java專項(xiàng)練習(xí)刷選擇題很合適LeetCode的熱題100可以覆蓋手寫代碼題JavaGuide這類開源資料適合梳理知識(shí)框架。不建議上來就刷一堆偏難怪題筆試考的還是金字塔底部那部分。7.2 臨場(chǎng)做題的節(jié)奏建議校招筆試通常有時(shí)間限制合理分配時(shí)間比盲目搶時(shí)間重要得多。選擇題遇到拿不準(zhǔn)的先標(biāo)記跳過別在一道題上卡超過兩分鐘。整套卷子里手寫代碼題的分值通常最高至少要留出三分之一的時(shí)間處理。一個(gè)比較穩(wěn)的節(jié)奏是快速掃描全卷先做有把握的題建立信心再做需要?jiǎng)幽X的計(jì)算題和代碼閱讀題最后集中攻克手寫題。手寫題如果一時(shí)間沒有完整思路先把框架搭出來方法簽名、主循環(huán)、遞歸或迭代的主體結(jié)構(gòu)再逐步完善細(xì)節(jié)。閱卷時(shí)通常按步驟給分一個(gè)能跑通主干但缺邊界處理的實(shí)現(xiàn)遠(yuǎn)好過一個(gè)寫了一半的猜想。7.3 我踩過的坑和最后想說的經(jīng)驗(yàn)在我接觸過的應(yīng)屆生里筆試翻車的常見原因往往不是不會(huì)而是基礎(chǔ)細(xì)節(jié)掌握得不精確。有人能背出HashMap的擴(kuò)容流程卻說不清i arr.length為什么不能寫成i arr.length有人能大談微服務(wù)架構(gòu)卻寫不對(duì)一個(gè)簡(jiǎn)單的深拷貝。這類金字塔塔尖很尖、地基很虛的情況在校招筆試?yán)锾貏e吃虧。我的真實(shí)感受是筆試準(zhǔn)備這件事沒有捷徑真題和熱詞只能告訴你考什么真正的得分點(diǎn)在于你能不能把每一個(gè)常見知識(shí)點(diǎn)都理解到能講給別人聽的程度。我背八股文的方法很簡(jiǎn)單遇到一個(gè)知識(shí)點(diǎn)試著用大白話給自己講一遍如果講著講著卡殼了說明這里還不夠熟就回去重新看。最后再分享一個(gè)小技巧準(zhǔn)備筆試時(shí)自己把??嫉氖謱戭}整理一個(gè)模板庫每道題都標(biāo)清楚時(shí)間復(fù)雜度和邊界條件??记耙煌聿灰⑿骂}只看模板。這套方法幫我度過了好幾場(chǎng)校招筆試也希望對(duì)你有用。