建面試題庫:從面經(jīng)收集到模擬面試全鏈路)
簡介這是一份面向Java求職者與在校生的企業(yè)面試真題合集圍繞Java基礎(chǔ)、高級特性、JVM、Spring框架、數(shù)據(jù)庫、設(shè)計模式與算法等高頻考點整理幫助讀者系統(tǒng)梳理知識脈絡(luò)、針對性備戰(zhàn)不同層級崗位的面試。資源包共895個文件以677張jpg截圖、103個txt筆記、73個docx與11個doc文檔為主另含少量png、m4a錄音、pdf及zip子包壓縮后約442.8MB涵蓋面經(jīng)記錄、筆試題與專項總結(jié)等多種形態(tài)。目前已有1727人學(xué)習(xí)下載內(nèi)容涉及JavaEE面試寶典、基礎(chǔ)常見面試總結(jié)及多家企業(yè)筆試真題可幫助讀者對照真實題目查漏補缺理解面試官考查思路并針對高級職位或移動端方向做專項準(zhǔn)備提升面試成功率。1. 面試真題收集這件事為什么你背了三百道還是掛面試真題收集聽起來像個體力活實際上是個數(shù)據(jù)工程問題。我見過太多人把面經(jīng)復(fù)制到記事本里按公司分文件夾結(jié)果面試前三天翻不到那道被問到的題。問題不在記憶力在于收集方式從一開始就錯了——你收集的是“題目文本”不是“可檢索、可分類、可驗證的結(jié)構(gòu)化數(shù)據(jù)”。用 Java 做這件事核心就三件事把散落在各處的面經(jīng)變成統(tǒng)一格式把題目按技術(shù)棧和難度打標(biāo)把答案和追問鏈補全。這套東西做出來你面對的不再是幾百道孤立的題而是一個能按“JVM 內(nèi)存模型”“Spring 循環(huán)依賴”“MySQL 索引失效”隨時調(diào)取的本地知識庫。適合誰正在準(zhǔn)備跳槽的 Java 后端帶新人需要題庫的 Tech Lead以及想把自己面試經(jīng)歷沉淀成資產(chǎn)的人。下面我從數(shù)據(jù)采集講到檢索優(yōu)化每一步都給能跑的代碼和參數(shù)。2. 從面經(jīng)文本到結(jié)構(gòu)化題目采集與清洗的完整鏈路2.1 為什么直接存 Markdown 是死路一條很多人收集面經(jīng)的第一步是新建一個面試題.md把看到的內(nèi)容粘進(jìn)去。三個月后這個文件超過五千行你想找“HashMap 擴(kuò)容”相關(guān)的題CtrlF 只能匹配到字面量如果原文寫的是“哈希表 resize”你就漏了。更致命的是你無法回答“我 Spring 相關(guān)的題準(zhǔn)備到什么程度了”這種統(tǒng)計問題。結(jié)構(gòu)化收集的本質(zhì)是把非結(jié)構(gòu)化文本拆成字段。一道面試題至少需要這些字段題干、技術(shù)棧標(biāo)簽、難度等級、出現(xiàn)頻次、答案要點、追問方向、來源公司類型。用 Java 做這件事我一般選 SQLite 做本地存儲零配置、單文件、支持全文索引比 JSON 文件靠譜得多。下面這張表是我實際用的字段設(shè)計你可以直接抄。字段名類型說明idINTEGER自增主鍵questionTEXT題干原文保留原始表述stackTEXT技術(shù)棧標(biāo)簽逗號分隔如JVM,GCdifficultyINTEGER1-51 為初級5 為專家frequencyINTEGER被問次數(shù)初始為 1answerTEXT答案要點支持 MarkdownfollowupsTEXT追問鏈JSON 數(shù)組source_typeTEXT來源類型大廠/中廠/外包/自研created_atTEXT首次錄入時間last_reviewTEXT上次復(fù)習(xí)時間注意stack字段不要用外鍵關(guān)聯(lián)單獨的表面試題的技術(shù)棧交叉嚴(yán)重一道題可能同時屬于 JVM 和并發(fā)用逗號分隔的文本配合 LIKE 查詢在幾千條量級下完全夠用過早規(guī)范化只會增加錄入負(fù)擔(dān)。2.2 用 Java 寫一個面經(jīng)解析器從復(fù)制文本到入庫假設(shè)你從某個面經(jīng)帖子里復(fù)制了一段文本格式大概是“1. 說說 JVM 內(nèi)存模型答分為堆、棧、方法區(qū)……”這種。手寫解析規(guī)則不現(xiàn)實因為不同來源格式差異太大。我的做法是先統(tǒng)一成“題干 答案”兩段式再用正則提取技術(shù)棧標(biāo)簽。import java.sql.*; import java.util.regex.*; public class InterviewParser { // 匹配 數(shù)字. 題干答答案 或 數(shù)字、題干答案 private static final Pattern QA_PATTERN Pattern.compile( ^\\d[.、]\\s*(.?[?])\\s*(?:答[:]\\s*)?(.)$, Pattern.DOTALL ); // 技術(shù)棧關(guān)鍵詞映射用于自動打標(biāo) private static final String[][] STACK_KEYWORDS { {JVM, 內(nèi)存|GC|垃圾回收|類加載|字節(jié)碼|堆|棧}, {并發(fā), 線程|鎖|synchronized|volatile|CAS|AQS|線程池}, {Spring, Bean|IOC|AOP|循環(huán)依賴|事務(wù)|SpringBoot}, {MySQL, 索引|事務(wù)隔離|MVCC|鎖|explain|慢查詢}, {Redis, 緩存|持久化|集群|過期|淘汰|分布式鎖}, {消息隊列, Kafka|RocketMQ|RabbitMQ|消息|消費|積壓}, {分布式, 一致性|CAP|分布式事務(wù)|注冊中心|限流|熔斷}, {網(wǎng)絡(luò), TCP|HTTP|三次握手|四次揮手|粘包|IO模型} }; public static void parseAndInsert(String rawText, String sourceType) throws SQLException { String[] lines rawText.split(\\n); try (Connection conn DriverManager.getConnection(jdbc:sqlite:interview.db)) { conn.setAutoCommit(false); String sql INSERT INTO questions(question, stack, difficulty, frequency, answer, source_type, created_at) VALUES(?,?,?,?,?,?,datetime(now)); try (PreparedStatement ps conn.prepareStatement(sql)) { for (String line : lines) { Matcher m QA_PATTERN.matcher(line.trim()); if (!m.find()) continue; String question m.group(1).trim(); String answer m.group(2).trim(); String stack detectStack(question answer); int difficulty estimateDifficulty(question, answer); ps.setString(1, question); ps.setString(2, stack); ps.setInt(3, difficulty); ps.setInt(4, 1); ps.setString(5, answer); ps.setString(6, sourceType); ps.addBatch(); } ps.executeBatch(); conn.commit(); } } } private static String detectStack(String text) { StringBuilder sb new StringBuilder(); for (String[] pair : STACK_KEYWORDS) { if (Pattern.compile(pair[1]).matcher(text).find()) { if (sb.length() 0) sb.append(,); sb.append(pair[0]); } } return sb.length() 0 ? 通用 : sb.toString(); } private static int estimateDifficulty(String q, String a) { // 簡單啟發(fā)題干長度 答案長度 是否含源碼/底層關(guān)鍵詞 int score 1; if (q.length() 30) score; if (a.length() 200) score; if (q.matches(.*(源碼|底層|原理|實現(xiàn)|設(shè)計).*)) score; return Math.min(score, 5); } }這段代碼的邏輯分三層。第一層是正則匹配QA_PATTERN處理“數(shù)字題干答”的常見格式DOTALL讓答案可以跨行。第二層是技術(shù)棧自動打標(biāo)STACK_KEYWORDS里每個技術(shù)棧對應(yīng)一組正則命中就追加標(biāo)簽這樣你錄入時不用手動選分類。第三層是難度估算用題干長度、答案長度和關(guān)鍵詞做啟發(fā)式打分雖然粗糙但比全部默認(rèn) 3 分強(qiáng)后續(xù)復(fù)習(xí)時可以按難度篩選。參數(shù)說明sourceType建議用固定枚舉值比如BIG_TECH、MID_TECH、OUTSOURCE、SELF_DEV方便后續(xù)按來源統(tǒng)計。frequency初始為 1后續(xù)遇到相同題目時用UPDATE questions SET frequency frequency 1 WHERE question ?累加。數(shù)據(jù)庫文件interview.db放在項目根目錄記得加.gitignore別把個人題庫提交到公開倉庫。2.3 去重與合并同一道題被問了八次怎么處理面經(jīng)收集到一定量后重復(fù)題會大量出現(xiàn)。比如“HashMap 底層原理”可能以“說說 HashMap”“HashMap 怎么擴(kuò)容”“HashMap 線程安全嗎”三種表述出現(xiàn)。完全靠字符串相等去重會漏掉變體靠語義相似度又太重。我的做法是兩級去重第一級用題干歸一化后的哈希第二級用技術(shù)棧關(guān)鍵詞重疊度。import java.security.MessageDigest; import java.util.*; public class Deduplicator { // 歸一化去掉標(biāo)點、空格、語氣詞轉(zhuǎn)小寫 public static String normalize(String question) { return question.replaceAll([?。,!、\\s], ) .replaceAll((說說|講講|談?wù)剕了解|知道|會嗎|是什么|為什么), ) .toLowerCase(); } public static String hash(String normalized) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(normalized.getBytes()); StringBuilder sb new StringBuilder(); for (byte b : digest) sb.append(String.format(%02x, b)); return sb.toString(); } catch (Exception e) { throw new RuntimeException(e); } } // 關(guān)鍵詞重疊度用于判斷兩道題是否在問同一件事 public static double overlap(String q1, String q2) { SetString s1 new HashSet(Arrays.asList(q1.split([\\s,]))); SetString s2 new HashSet(Arrays.asList(q2.split([\\s,]))); if (s1.isEmpty() || s2.isEmpty()) return 0; SetString intersection new HashSet(s1); intersection.retainAll(s2); return (double) intersection.size() / Math.min(s1.size(), s2.size()); } }歸一化那一步是關(guān)鍵。normalize方法把“說說 HashMap 的底層原理”變成“hashmap底層原理”這樣不同語氣詞的變體就能命中同一個哈希。但光靠哈希不夠因為“HashMap 擴(kuò)容”和“HashMap 原理”歸一化后不同實際是包含關(guān)系。所以第二級用overlap算關(guān)鍵詞重疊度超過 0.6 就認(rèn)為是同一道題的變體合并時保留題干更完整的那條frequency累加。注意去重不要追求 100% 準(zhǔn)確寧可漏合并也不要錯合并。把“JVM 內(nèi)存模型”和“JVM 垃圾回收”合并成一道題比保留兩道獨立的題危害大得多。我的經(jīng)驗是 overlap 閾值設(shè) 0.6 到 0.7 之間低于 0.6 的變體手動確認(rèn)。3. 給題目打標(biāo)簽與建索引讓檢索從分鐘級降到秒級3.1 技術(shù)棧標(biāo)簽體系怎么設(shè)計才不混亂自動打標(biāo)只能解決 70% 的場景剩下 30% 需要手動補。但手動補的前提是標(biāo)簽體系本身不能太細(xì)。我見過有人把標(biāo)簽分成“JVM-內(nèi)存-JMM-可見性”四級結(jié)果錄入時自己都記不住該選哪個。我的建議是兩級標(biāo)簽一級是技術(shù)域二級是具體知識點但二級標(biāo)簽不強(qiáng)制填用題干里的關(guān)鍵詞自然覆蓋。一級標(biāo)簽固定為這些Java基礎(chǔ)、JVM、并發(fā)、Spring、MySQL、Redis、消息隊列、分布式、網(wǎng)絡(luò)、算法、設(shè)計模式、系統(tǒng)設(shè)計、通用。每個一級標(biāo)簽下可以有關(guān)鍵詞列表用于自動匹配。比如JVM的關(guān)鍵詞包括“內(nèi)存模型、GC、垃圾回收、類加載、字節(jié)碼、堆、棧、方法區(qū)、JMM、happens-before”。這套體系的好處是你復(fù)習(xí)時可以按域推進(jìn)不會陷入細(xì)節(jié)。-- 創(chuàng)建全文索引支持題干和答案的模糊檢索 CREATE VIRTUAL TABLE questions_fts USING fts5( question, answer, stack, contentquestions, content_rowidid ); -- 觸發(fā)器插入時同步到 FTS 表 CREATE TRIGGER questions_ai AFTER INSERT ON questions BEGIN INSERT INTO questions_fts(rowid, question, answer, stack) VALUES (new.id, new.question, new.answer, new.stack); END; -- 查詢示例找 JVM 相關(guān)且答案里提到“可達(dá)性分析”的題 SELECT q.* FROM questions q JOIN questions_fts f ON q.id f.rowid WHERE questions_fts MATCH stack:JVM AND answer:可達(dá)性分析 ORDER BY q.frequency DESC;FTS5 是 SQLite 的全文索引模塊比 LIKE 快一個數(shù)量級。contentquestions表示外部內(nèi)容表模式不重復(fù)存儲數(shù)據(jù)。觸發(fā)器保證插入時自動同步索引。查詢時MATCH支持列過濾和布爾組合stack:JVM限定技術(shù)棧answer:可達(dá)性分析限定答案內(nèi)容。按frequency降序排列高頻題優(yōu)先看。3.2 用 Java 封裝檢索接口按技術(shù)棧難度頻次組合查詢直接寫 SQL 查詢對使用者不友好封裝一個 Java 方法輸入技術(shù)棧、難度范圍、關(guān)鍵詞返回排序后的題目列表。import java.sql.*; import java.util.*; public class QuestionRepository { private final Connection conn; public QuestionRepository(String dbPath) throws SQLException { this.conn DriverManager.getConnection(jdbc:sqlite: dbPath); } public ListQuestion search(String stack, int minDiff, int maxDiff, String keyword) throws SQLException { StringBuilder sql new StringBuilder( SELECT q.* FROM questions q JOIN questions_fts f ON q.id f.rowid WHERE 11 ); ListObject params new ArrayList(); if (stack ! null !stack.isEmpty()) { sql.append( AND q.stack LIKE ?); params.add(% stack %); } sql.append( AND q.difficulty BETWEEN ? AND ?); params.add(minDiff); params.add(maxDiff); if (keyword ! null !keyword.isEmpty()) { sql.append( AND questions_fts MATCH ?); params.add(keyword); } sql.append( ORDER BY q.frequency DESC, q.difficulty DESC LIMIT 50); try (PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } ResultSet rs ps.executeQuery(); ListQuestion result new ArrayList(); while (rs.next()) { Question q new Question(); q.id rs.getInt(id); q.question rs.getString(question); q.stack rs.getString(stack); q.difficulty rs.getInt(difficulty); q.frequency rs.getInt(frequency); q.answer rs.getString(answer); result.add(q); } return result; } } }search方法的參數(shù)設(shè)計對應(yīng)四種復(fù)習(xí)場景。只傳stack是“我要過一遍 JVM 的題”傳minDiff4, maxDiff5是“只看高級題”傳keyword是“找和可達(dá)性分析相關(guān)的題”三個都傳就是精確打擊。LIMIT 50防止一次返回太多復(fù)習(xí)時看不過來。ORDER BY frequency DESC讓高頻題排前面這是最實用的排序策略——被問得多的題你大概率還會遇到。注意stack LIKE %JVM%在數(shù)據(jù)量超過一萬條時會變慢如果題庫很大建議把stack字段拆成關(guān)聯(lián)表或者用 FTS5 的stack:JVM語法替代 LIKE。幾千條以內(nèi) LIKE 完全夠用不要過早優(yōu)化。3.3 答案與追問鏈的補全策略收集題干只是第一步真正決定面試表現(xiàn)的是答案質(zhì)量和追問深度。我的做法是每道題錄入時答案字段只寫“要點骨架”不寫完整段落。比如“JVM 內(nèi)存模型”的答案骨架是“堆、棧、方法區(qū)、程序計數(shù)器、本地方法棧 各區(qū)域作用 可能拋出的異常”。追問鏈單獨存 JSON記錄“如果面試官接著問什么我該怎么答”。// 追問鏈的 JSON 結(jié)構(gòu)示例 String followups [ {q: 堆和棧的區(qū)別, a: 堆線程共享棧線程私有堆存對象棧存局部變量和引用}, {q: 方法區(qū)在 JDK8 之后有什么變化, a: 永久代被元空間替代元空間使用本地內(nèi)存}, {q: 什么情況下會 OOM, a: 堆 OOM、棧溢出、元空間 OOM、直接內(nèi)存 OOM} ] ;這種結(jié)構(gòu)的價值在于你復(fù)習(xí)時不是背一道題而是背一條追問鏈。面試官問“JVM 內(nèi)存模型”你答完基礎(chǔ)他大概率會追問“堆和棧的區(qū)別”你提前準(zhǔn)備好了節(jié)奏就在你手里。追問鏈的 JSON 可以直接存 TEXT 字段查詢時用 Java 的 JSON 庫解析不需要額外建表。4. 避坑與排查題庫建設(shè)中最容易翻車的五個點4.1 現(xiàn)象錄入時自動打標(biāo)準(zhǔn)確率越來越低原因STACK_KEYWORDS里的正則太寬泛比如“鎖”這個關(guān)鍵詞同時命中“并發(fā)”和“MySQL”導(dǎo)致一道 MySQL 鎖的題被打上并發(fā)標(biāo)簽。解決給關(guān)鍵詞加權(quán)重或者用更精確的短語。比如把“鎖”改成“synchronized|ReentrantLock|AQS|CAS”用于并發(fā)MySQL 的鎖用“行鎖|表鎖|間隙鎖|MVCC”。定期抽查自動打標(biāo)結(jié)果發(fā)現(xiàn)誤標(biāo)就收緊正則。4.2 現(xiàn)象FTS5 查詢報no such column: stack原因FTS5 外部內(nèi)容表模式下MATCH的列名必須和 FTS 表定義的列一致。如果questions_fts只定義了question, answer, stack但查詢時寫了stack:JVM而stack列在 FTS 表里存在通常不會報這個錯。真正的原因是觸發(fā)器沒建FTS 表里沒數(shù)據(jù)或者contentquestions寫成了contentquestions。解決先SELECT count(*) FROM questions_fts確認(rèn)索引有數(shù)據(jù)再檢查觸發(fā)器是否生效。4.3 現(xiàn)象去重時把不同難度的題合并了原因overlap只看關(guān)鍵詞重疊不看難度。一道“HashMap 原理”難度 2一道“HashMap 擴(kuò)容死循環(huán)”難度 4關(guān)鍵詞重疊度高被合并成一道結(jié)果復(fù)習(xí)時漏掉了高難度題。解決去重時增加難度差判斷Math.abs(diff1 - diff2) 1就不合并?;蛘吆喜r保留兩條記錄用parent_id關(guān)聯(lián)查詢時按難度展開。4.4 現(xiàn)象答案字段越寫越長檢索變慢原因把完整文章粘進(jìn)answer字段單條記錄超過 10KBFTS5 索引膨脹。解決答案只存要點完整內(nèi)容存到單獨的文件數(shù)據(jù)庫里存文件路徑?;蛘哂胊nswer_summary和answer_detail兩個字段檢索用 summary復(fù)習(xí)時按需加載 detail。4.5 現(xiàn)象復(fù)習(xí)時不知道該看哪些題原因沒有復(fù)習(xí)狀態(tài)跟蹤。last_review字段錄入了但沒用起來。解決加一個review_count字段每次復(fù)習(xí)后加一查詢時按last_review升序排列優(yōu)先看最久沒復(fù)習(xí)的題。再配合frequency加權(quán)高頻且久未復(fù)習(xí)的題排最前。-- 復(fù)習(xí)優(yōu)先級排序高頻 久未復(fù)習(xí) 高難度 SELECT *, (frequency * 2 difficulty - julianday(now) julianday(last_review)) AS priority FROM questions WHERE last_review IS NOT NULL ORDER BY priority DESC LIMIT 20;這個排序公式里frequency * 2讓高頻題權(quán)重翻倍difficulty讓高難度題靠前julianday差值讓久未復(fù)習(xí)的題優(yōu)先。last_review為空的新題不參與排序單獨用WHERE last_review IS NULL查出來先過一遍。5. 把題庫變成面試模擬器一個可驗證的進(jìn)階技巧題庫建好之后最有效的驗證方式不是“看一遍”而是“講一遍”。我給自己寫了一個簡單的模擬器隨機(jī)抽 10 道題按技術(shù)棧分組要求自己在不看答案的情況下口述然后用錄音轉(zhuǎn)文字對比答案要點。這個過程中我發(fā)現(xiàn)很多題“看的時候會”“講的時候卡”??ㄗ〉牡胤骄褪钦嬲谋∪觞c。具體做法是用 Java 寫一個MockInterview類從數(shù)據(jù)庫按技術(shù)棧權(quán)重抽題。權(quán)重規(guī)則frequency越高權(quán)重越大last_review越久權(quán)重越大difficulty在 3 到 4 之間的題權(quán)重最高太簡單的沒必要練太難的面試中很少被問到。public class MockInterview { public static ListQuestion drawQuestions(Connection conn, int count) throws SQLException { String sql SELECT *, (frequency * 1.5 difficulty * 0.8 (julianday(now) - julianday(COALESCE(last_review, 2020-01-01))) * 0.1) AS weight FROM questions WHERE difficulty BETWEEN 2 AND 5 ORDER BY weight DESC LIMIT ? ; // 實際使用時加隨機(jī)因子避免每次抽同樣的題 try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, count * 3); // 先取三倍候選再隨機(jī)選 ResultSet rs ps.executeQuery(); ListQuestion candidates new ArrayList(); while (rs.next()) { Question q new Question(); q.question rs.getString(question); q.answer rs.getString(answer); q.stack rs.getString(stack); candidates.add(q); } Collections.shuffle(candidates); return candidates.subList(0, Math.min(count, candidates.size())); } } }抽題之后我會用手機(jī)錄音口述每道題的答案然后回放對照數(shù)據(jù)庫里的答案要點。這個過程很痛苦但效果比反復(fù)看強(qiáng)得多??谑鰰r你會發(fā)現(xiàn)有些題你只能說出關(guān)鍵詞串不成句子有些追問鏈你只記得第一層第二層就斷了。這些就是需要重點補的地方。另一個驗證方法是“交叉出題”把 A 技術(shù)棧的題用 B 技術(shù)棧的視角回答。比如“Redis 持久化”這道題試著從“如果讓你設(shè)計一個持久化機(jī)制”的角度講而不是背 RDB 和 AOF 的區(qū)別。這種練法能暴露你對原理的理解深度面試官稍微換個問法你就不行了。注意模擬器抽題后不要立刻看答案。先口述再對照最后把差異點補進(jìn)answer字段。這個循環(huán)跑上五輪你的答案要點會越來越精煉追問鏈會越來越完整。我自己的習(xí)慣是每周跑一次模擬每次 10 道題錄音存到本地一個月后回聽第一周的錄音能明顯感覺到表達(dá)流暢度的變化。題庫的價值不在于收集了多少題而在于你能否在壓力下把每道題講清楚。這套 Java 實現(xiàn)的收集、清洗、索引、模擬鏈路從第一行代碼到能跑模擬我用了兩個周末之后每次跳槽前只需要更新面經(jīng)和跑模擬不用再從頭整理。希望幫到你。本文還有配套的精品資源點擊獲取