考點全解析)
聊到用友2018秋招Java筆試題四我印象最深的不是哪道題特別難而是整套卷子的節(jié)奏感。前半小時你會覺得“就這”后半小時開始懷疑自己是不是漏看了什么條件交卷前十分鐘發(fā)現(xiàn)好幾道題都是“看似基礎實際埋雷”。這套題是我當年秋招做過的Java筆試卷里非常能代表企業(yè)級應用廠商考察Java工程師風格的一套既有大量基礎題刷掉背書黨又有幾道需要真正寫過代碼才能答對的題還有一兩道能區(qū)分“背過八股”和“真懂原理”的題。這篇不像網上那些只貼題目和答案的帖子我會按我記憶里的卷面順序把高頻考點、易錯點、擴展追問全部串起來講一遍。無論你是準備校招、社招跳槽還是純粹想把Java基礎打扎實這套題都值得認真過一遍。1. 先把卷面攤開這套題到底在考什么1.1 題量與題型分布用友這套秋招筆試題的卷面我當時拿到的是紙質版有的城市是機房在線答題時間兩個小時總分100分。題型大致是這樣的題型題量分值我的感受單選題15題30分覆蓋面廣JavaSE為主少量數(shù)據(jù)庫和網絡多選題5題15分最惡心少選多選都扣分簡答題3題15分類和對象、集合、并發(fā)三選二編程題2題25分一道數(shù)據(jù)結構一道簡單業(yè)務邏輯設計SQL題1題15分三表關聯(lián)查詢還要求寫優(yōu)化思路說實話這個分布在一眾互聯(lián)網大廠里不算激進。頭部的電商、社交大廠那時候已經開始狂問分布式、中間件、高并發(fā)場景題了用友這張卷子還是偏“底子”的。但別小看這種卷子它對細節(jié)的摳法比直接問“Redis為什么快”要陰險得多。1.2 明顯送分題與拉開差距的題送分題基本集中在單選前10題比如“Java的基本數(shù)據(jù)類型有幾種”“下列哪個關鍵字用于異常處理”“ArrayList和LinkedList哪個查詢更快”。只要系統(tǒng)學過Java這部分閉著眼也能拿分。真正拉開差距的是后面幾類多選題里經常出現(xiàn)“下列說法正確的是”每個選項都長得像對的實際至少有一個選項是錯的。簡答題不直接問“了解集合嗎”而是問“HashMap在JDK 8中鏈表轉紅黑樹的閾值為什么是8為什么不是7或9”。編程題不考“反轉字符串”而是考“從文件中讀取訂單數(shù)據(jù)按金額排序并輸出”需要你寫出完整可運行的代碼包括異常處理。SQL題除了一句查詢外還要求你寫出索引建議并解釋為什么這樣建索引。如果你只是刷過別人的面經沒有真正自己敲過代碼、看過源碼這套卷子能把你戳得很疼。1.3 兩個小時內怎么分配時間我自己當時的時間分配是選擇題30分鐘多選題10分鐘簡答題20分鐘編程題40分鐘SQL題15分鐘剩下5分鐘檢查。但實際做下來選擇題花了快40分鐘因為有幾道題需要實際推算比如二叉樹深度、HashMap擴容后的下標位置。如果你將來也要參加這類筆試我的建議是選擇題遇到不會的果斷標記不要戀戰(zhàn)一道題最多兩分鐘。多選題寧可少選不要多選很多題目規(guī)則是“全部選對得滿分選對但不全得一半分多選或錯選不得分”。編程題先寫解題思路注釋再寫代碼至少保證核心邏輯的分能拿到。SQL題務必用縮進把層次寫清楚閱卷人一眼能看到你的連接條件。2. 逐題復盤選擇題里的高頻考點與易錯細節(jié)2.1 集合框架HashMap的容量、哈希碰撞、紅黑樹閾值選擇題里有一道我記得特別清楚HashMap默認初始容量是多少選項有8、10、16、32。答案是16。但真正有水平的追問是后面那題為什么是16而不是10為什么加載因子是0.75這里展開說。HashMap的容量被設計成2的冪次方是為了能用(n - 1) hash位運算代替取模速度更快。162^4所以默認容量是16。如果你傳入一個不是2的冪次方的初始容量比如19HashMap內部會用tableSizeFor方法把它轉換成大于等于19的最小2的冪次方也就是32。加載因子0.75是空間和時間的折中。調大了比如1.0哈希沖突會更嚴重查詢變慢調小了比如0.5空間浪費明顯。0.75這個數(shù)值在泊松分布模型下桶內鏈表長度達到8的概率已經非常小所以鏈表轉紅黑樹的閾值是8轉回鏈表的閾值是6避免在樹和鏈表之間頻繁切換。這道題的延伸問到過“JDK 8的HashMap和JDK 7有什么區(qū)別”。你至少要能答出三點JDK 8引入紅黑樹JDK 7沒有。JDK 8插入用尾插法JDK 7用頭插法。JDK 7擴容時會重新計算hashJDK 8通過高位運算簡化了計算。這些都屬于“你真正看過源碼才答得出”的細節(jié)。我建議你把JDK 8 HashMap的put、get、resize三個方法的源碼讀透因為后續(xù)技術面的追問基本都是從這里長出來的。2.2 String與包裝類 和 equals 的坑單選題里有一道非常經典的String s1 new String(abc); String s2 abc; System.out.println(s1 s2); System.out.println(s1.equals(s2));第一個輸出false第二個輸出true。這個大多數(shù)人都知道。但用友這道題又加了一層String s3 a bc; String s4 abc; System.out.println(s3 s4);答案是true因為編譯期常量折疊a bc會被編譯器直接優(yōu)化成常量abc所以s3和s4都指向常量池里同一個對象。比這個更陰的是改個寫法String s5 a; String s6 s5 bc; System.out.println(s6 s4);答案是false。因為s5是變量變量的拼接在運行期通過StringBuilder完成生成的是堆里的新對象。同理Integer的緩存范圍也有類似的坑。Integer a 127; Integer b 127; a b是true但如果是128就走緩存了不對-128到127之間是緩存128超出范圍會new兩個不同對象所以128那個比較是false。這類題的本質不是考語法而是考你對JVM內存模型和編譯器優(yōu)化機制的熟悉程度。我當時在這道題上多留了個心眼把常量池、堆、棧之間的關系在腦子里又過了一遍后面JVM的題答起來就順多了。2.3 并發(fā)編程volatile、synchronized與線程池有題問volatile關鍵字能保證什么。選項里有可見性、有序性、原子性。正確選項是可見性和有序性它不能保證原子性。很多剛學Java的人都以為volatile和synchronized差不多其實是兩個層面的東西。volatile解決的是“多線程之間某個變量的修改對其他線程可見”的問題。它通過內存屏障禁止指令重排序讀的時候強制從主內存讀寫的時候強制刷回主內存。但count這種操作分三步讀、加一、寫回volatile管不了這三步的原子性。所以并發(fā)編程題里凡是看到volatile int count然后多線程做自增的代碼問結果是多少你都要明白答案是不確定的。另一個常考的是synchronized和ReentrantLock的區(qū)別。我當時把要點寫成了對比synchronized是關鍵字ReentrantLock是類。synchronized是隱式鎖自動釋放ReentrantLock需手動lock和unlock。synchronized是非公平鎖ReentrantLock默認非公平但可設置為公平鎖。ReentrantLock支持tryLock超時等待支持多個Condition條件。JDK 6之后synchronized經過鎖升級優(yōu)化性能已經不輸ReentrantLock。還有線程池的題問ThreadPoolExecutor的核心參數(shù)里workQueue放的是什么。答案是等待執(zhí)行的任務。很多選擇題把workQueue和threadFactory混淆實際上threadFactory是創(chuàng)建線程的工廠。2.4 JVM內存區(qū)域與類加載JVM的題在這套卷子里占了不小比重。選擇題問“下列哪個區(qū)域不屬于線程共享”選項有堆、方法區(qū)、虛擬機棧、元數(shù)據(jù)區(qū)。答案是虛擬機棧它是線程私有的。緊接著的簡答題是“描述一下類加載的雙親委派模型為什么需要它”雙親委派的意思是一個類加載器收到類加載請求后不會自己先去加載而是把請求委派給父加載器逐級向上最后才由自己加載。它的好處有三個避免類被重復加載比如你自定義了一個java.lang.String因為雙親委派最終會由啟動類加載器加載JDK自帶的String避免核心類被覆蓋。保證Java類型體系的安全。避免不同類加載器加載出同名類導致混亂。這道題背后還藏著一個考點如果你自己寫了一個java.lang.String然后放到classpath下能加載成功嗎答案是不能會拋SecurityException因為雙親委派機制攔截了它。這也是為什么很多面試官喜歡順著雙親委派問Tomcat的類加載器為什么要打破雙親委派。Tomcat需要為不同Web應用提供隔離的類加載環(huán)境所以它自己實現(xiàn)了一套WebAppClassLoader先加載自己WEB-INF/classes下的類再委派給父加載器。2.5 Spring與數(shù)據(jù)庫企業(yè)級應用繞不開的部分作為企業(yè)級應用廠商用友筆試里Spring和數(shù)據(jù)庫肯定不會缺席。Spring的題目有一道是“Spring Bean的默認作用域是什么”。答案是singleton單例。然后問“prototype作用域適合什么場景”。適合有狀態(tài)的對象比如一個對象里面存了當前用戶信息如果用單例多線程并發(fā)訪問會互相污染數(shù)據(jù)。還有一道事務傳播行為的題“ServiceA的methodA調用ServiceB的methodBmethodA的傳播行為是REQUIREDmethodB的傳播行為是REQUIRES_NEW如果methodB拋了異常methodA已經執(zhí)行的部分會回滾嗎”答案是methodA已執(zhí)行的部分不會因為methodB的回滾而回滾因為REQUIRES_NEW會開啟一個新事務新事務的執(zhí)行結果與外部事務無關。但如果methodB異常向上拋出且methodA捕獲后不再拋異常methodB新事務回滾methodA自己的事務因為沒有拋出RuntimeException可能正常提交。數(shù)據(jù)庫的題經典的是“為什么InnoDB的索引要用B樹而不是二叉搜索樹或哈希表”。參考答案是二叉搜索樹在極端情況下會退化成鏈表樹高度過高磁盤IO次數(shù)增加。哈希表雖然單點查詢O(1)但不支持范圍查詢和排序。B樹是多路平衡樹高度低一般三層左右就能存儲千萬級數(shù)據(jù)一次查詢只需要3到4次磁盤IO。B樹的數(shù)據(jù)都存儲在葉子節(jié)點并且葉子節(jié)點之間用指針連接非常適合范圍查詢。還有一道事務隔離級別的多選題問你InnoDB默認的隔離級別是什么。答案是REPEATABLE READ可重復讀。但InnoDB通過MVCC和間隙鎖在可重復讀下避免了幻讀所以實際隔離效果接近串行化。3. 編程題與SQL題真正拉分的是手寫能力3.1 手寫編程題的評分邏輯編程題不是只給代碼就滿分。閱卷老師會看你的解題思路、邊界條件、時間復雜度分析。我當時就吃過虧題目要求“找出整數(shù)數(shù)組中連續(xù)遞增的最長子序列長度”我直接寫了一個雙重循環(huán)答案對了但時間復雜度O(n^2)評論區(qū)都說應該一次遍歷O(n)搞定。后來我養(yǎng)成了一個習慣任何算法題先寫注釋說明暴力解法然后優(yōu)化為最優(yōu)解最后再寫代碼。比如求最長連續(xù)遞增子序列長度public int findLengthOfLCIS(int[] nums) { if (nums null || nums.length 0) { return 0; } int maxLen 1; int curLen 1; for (int i 1; i nums.length; i) { if (nums[i] nums[i - 1]) { curLen; } else { maxLen Math.max(maxLen, curLen); curLen 1; } } return Math.max(maxLen, curLen); }這段代碼的核心是“當連續(xù)遞增中斷時重置curLen為1”。很多初學者會忘記在循環(huán)結束后再更新一次maxLen導致整個數(shù)組都是遞增的用例出錯。這種邊界條件筆試里最容易扣分。另外一道題是“反轉單鏈表要求迭代和遞歸兩種方式”。迭代寫法就是三個指針prev、cur、next依次挪動public ListNode reverseList(ListNode head) { ListNode prev null; ListNode cur head; while (cur ! null) { ListNode next cur.next; cur.next prev; prev cur; cur next; } return prev; }遞歸寫法簡潔但容易繞暈public ListNode reverseList(ListNode head) { if (head null || head.next null) { return head; } ListNode newHead reverseList(head.next); head.next.next head; head.next null; return newHead; }我當時先用迭代法寫然后在注釋里補充遞歸思路保證至少一種寫法完整。筆試題量大能完整寫出一種就不錯了別貪多。3.2 SQL題三表關聯(lián)與索引優(yōu)化SQL題給的是員工表、部門表、薪資表三張表要求查每個部門薪資最高的員工姓名和薪資。這類題有兩種典型寫法。MySQL 8.0及以上可以用窗口函數(shù)SELECT department_name, employee_name, salary FROM ( SELECT d.name AS department_name, e.name AS employee_name, s.salary, ROW_NUMBER() OVER (PARTITION BY d.id ORDER BY s.salary DESC) AS rn FROM employee e JOIN department d ON e.dept_id d.id JOIN salary s ON e.id s.employee_id ) t WHERE rn 1;如果用的是MySQL 5.7不支持窗口函數(shù)就要用關聯(lián)子查詢或臨時表SELECT d.name AS department_name, e.name AS employee_name, s.salary FROM employee e JOIN department d ON e.dept_id d.id JOIN salary s ON e.id s.employee_id WHERE s.salary ( SELECT MAX(s2.salary) FROM salary s2 JOIN employee e2 ON s2.employee_id e2.id WHERE e2.dept_id d.id );這道題的坑在于如果兩個員工薪資相同且都是部門最高第一種子查詢會返回多條記錄第二種窗口函數(shù)能保證只取一條。筆試時如果沒特別說明“只取一人”我建議用窗口函數(shù)或者明確寫出“如果有并列則取第一條”。索引優(yōu)化思路也是采分點。我當時這樣寫的員工表的dept_id建普通索引。薪資表的employee_id建唯一索引或普通索引。如果按照部門維度統(tǒng)計較頻繁考慮聯(lián)合索引(dept_id, salary)。閱卷人看到你能主動寫出索引建議基本這題就拿了大半的分。3.3 業(yè)務設計題從訂單數(shù)據(jù)中提取關鍵信息還有一道編程題不是純算法而是描述一個場景某電商系統(tǒng)每天產生大量訂單要求你設計一個方法從訂單列表中統(tǒng)計出每個商品的銷售總金額并按照金額從高到低輸出。這題本質是考Map的用法和排序但很多人一上來就用HashMapString, BigDecimal存金額忘了考慮BigDecimal相加要用add而不是加號。我當時是這樣寫的public ListMap.EntryString, BigDecimal calculateTotalAmount(ListOrder orders) { MapString, BigDecimal totalMap new HashMap(); for (Order order : orders) { totalMap.put( order.getProductId(), totalMap.getOrDefault(order.getProductId(), BigDecimal.ZERO) .add(order.getAmount()) ); } ListMap.EntryString, BigDecimal result new ArrayList(totalMap.entrySet()); result.sort((e1, e2) - e2.getValue().compareTo(e1.getValue())); return result; }這里用了getOrDefault和BigDecimal的add而不是totalMap.put(key, totalMap.get(key) amount)因為double的精度在做金額計算時會有問題。這道題考察的就是“你會不會在業(yè)務代碼里用正確的方式處理精度”。4. 那些容易丟分的細節(jié)閱卷視角下的采分點4.1 手寫代碼的邊界條件最容易被扣筆試閱卷人最喜歡看的是你寫不寫空指針判斷、數(shù)組越界判斷、循環(huán)結束后是否更新最大值。每次面試復盤我都發(fā)現(xiàn)丟分點往往不在主體邏輯上而在以下幾個位置數(shù)組題沒有判空和長度0。鏈表題沒有處理head為null。字符串題沒有處理空串和null。遞歸題忘記停止條件。金額計算用了float/double而不是BigDecimal。一個很實用的習慣是寫完代碼后自己腦內跑三個用例——空輸入、單元素輸入、正常輸入。如果是鏈表再加一個兩個節(jié)點的用例。這一條能幫你少丟至少三分。4.2 簡答題的“廢話過多”問題很多人誤以為簡答題寫得多就分高其實閱卷是看關鍵詞給分的。比如讓你說“HashMap的put流程”你得先寫“計算hash - 定位桶 - 如果桶為空直接放 - 如果桶不為空判斷是否鏈表或紅黑樹 - 鏈表遍歷查找存在則覆蓋不存在則尾插 - 超過8轉紅黑樹 - 擴容判斷”。你把這些關鍵詞寫全然后稍微展開說明比長篇大論講HashMap歷史強得多。我當時吃了個虧有一道題問“JDK 8中HashMap為什么要引入紅黑樹”我洋洋灑灑寫了半頁紙從哈希函數(shù)講到泊松分布但漏了“解決鏈表過長導致的查詢退化為O(n)問題”這個核心句。所以后來我總結出一個答題模板先給結論再寫理由最后加一個例子。結論先行閱卷人掃一眼就能看到你的答案。4.3 多選題的“寧可少選”策略用友這套卷子多選題規(guī)則我記得是少選得一半分多選、錯選不得分。所以遇到拿不準的選項我的策略是只選最確定的那個。比如題目問“下列哪些是線程安全的集合”我確定ConcurrentHashMap是但拿不準Collections.synchronizedList包裝后的List算不算那我只選ConcurrentHashMap至少保住一半分。這種策略雖然激進但比平均分高。如果你追求滿分那就要把每個集合類的底層結構都梳理一遍。4.4 SQL題的隱性問題SQL題還有一個很容易忽略的點表連接時如果字段名有歧義必須加上表別名。比如員工表和薪資表都有employee_id字段你直接寫WHERE employee_id s.employee_id數(shù)據(jù)庫會報錯。這種低級錯誤在筆試里非常常見。另外要求“寫出優(yōu)化思路”的題不要只寫“加索引”。你要能寫出哪張表的哪個字段需要加索引。避免使用select *只查詢需要的字段。大數(shù)據(jù)量分頁時避免offset過大用延遲關聯(lián)。這些才是閱卷人想看到的優(yōu)化能力。5. 從這套題反推用友的招聘偏好與后續(xù)面試銜接5.1 筆試通過后面試會怎么追問做完這套題后我最大的感受是用友的面試官問的問題幾乎都從筆試題往外擴。比如筆試考了HashMap面試時就問“如果自定義對象作為HashMap的key需要注意什么”答案是要重寫equals和hashCode保證equals相等時hashCode也相等。筆試考了事務傳播行為面試就讓你現(xiàn)場說一個REQUIRES_NEW的實際使用場景。我當時舉的例子是在日志記錄場景中哪怕主業(yè)務事務回滾日志也必須記錄成功所以日志記錄方法要設置為REQUIRES_NEW。筆試考了JVM內存區(qū)域面試就讓你分析String s new String(abc)之后堆、棧、常量池里各有什么。所以你是為了面試準備的話刷這套筆試題不能只對答案你要把每個考點都延伸出至少三個追問自己用A4紙把答案寫一遍。這樣筆試過了面試環(huán)節(jié)也能順勢銜接上。5.2 企業(yè)在招聘后端Java工程師時其實是在挑什么人用友屬于企業(yè)級軟件頭部廠商產品線包括ERP、財務軟件、人力云等。這些系統(tǒng)的特點是業(yè)務復雜、數(shù)據(jù)量大、事務要求嚴格、系統(tǒng)不能輕易宕機。所以筆試里大量考察集合、并發(fā)、JVM、數(shù)據(jù)庫本質上是在篩選兩類能力一是基礎能力扎實能處理日常開發(fā)中遇到的并發(fā)問題、內存問題、SQL性能問題。二是業(yè)務建模能力能把復雜的訂單、報表、審批流轉換成清晰的代碼結構。這和互聯(lián)網大廠“上來就考算法、手撕紅黑樹”的風格差別很大。你準備用友這類公司的筆試題重點應該放在Java基礎、Spring、數(shù)據(jù)庫、企業(yè)級應用設計上而不是死磕DP和貪心。5.3 簡歷上怎么呼應這套題的考察點如果你投的是用友的Java開發(fā)崗簡歷上的技術棧描述一定要覆蓋這張卷子的核心考察點。我建議在簡歷里明確寫出熟練掌握Java集合框架研究過HashMap底層結構及擴容機制。熟悉JVM內存模型、垃圾回收算法有JVM調優(yōu)經驗。熟悉MySQL InnoDB存儲引擎掌握索引優(yōu)化、事務隔離級別。熟悉Spring事務傳播行為能處理分布式事務場景。這些句子看起來簡單但每一個都對應筆試的一道題。面試官看到你的簡歷描述和筆試題高度重合對你后續(xù)面試的提問起點也會友好很多。5.4 我后來復盤這套題時的幾點體會筆試結束當晚我把所有錯題和模糊題重新整理了一遍發(fā)現(xiàn)真正丟分的不是知識盲區(qū)而是這三件事多選題太貪心總想著拿滿分結果扣分。手寫算法題沒寫注釋邏輯跳步閱卷人讀起來費勁。SQL關鍵字沒大寫表連接字段沒加別名被扣了印象分。這些都是可以在考前通過模擬訓練規(guī)避的問題。所以我強烈建議你筆試前完整地做兩套題型類似的卷子用手機計時模擬真實考場環(huán)境。你會發(fā)現(xiàn)“會做”和“在兩個小時內做完”完全是兩回事。最后分享一個小技巧做筆試的時候不要直接在題目旁邊寫答案先在草稿紙上列關鍵詞。比如看到HashMap立刻寫下“數(shù)組鏈表紅黑樹、加載因子0.75、擴容2倍、頭插尾插區(qū)別”看到事務立刻寫下“ACID、傳播行為、隔離級別、MVCC”。這些關鍵詞既是你的答題大綱也是你檢查答案時的對照清單。很多題只需要你看到關鍵詞就能拼出正確答案遠比臨時回憶靠譜。