简介:这是一份面向Java求职者与在校生的企业面试真题合集,围绕Java基础、高级特性、JVM、Spring框架、数据库、设计模式与算法等高频考点整理,帮助读者系统梳理知识脉络、针对性备战不同层级岗位的面试。资源包共895个文件,以677张jpg截图、103个txt笔记、73个docx与11个doc文档为主,另含少量png、m4a录音、pdf及zip子包,压缩后约442.8MB,涵盖面经记录、笔试题与专项总结等多种形态。目前已有1727人学习下载,内容涉及JavaEE面试宝典、基础常见面试总结及多家企业笔试真题,可帮助读者对照真实题目查漏补缺,理解面试官考查思路,并针对高级职位或移动端方向做专项准备,提升面试成功率。
1. 面试真题收集这件事,为什么你背了三百道还是挂
面试真题收集,听起来像个体力活,实际上是个数据工程问题。我见过太多人把面经复制到记事本里,按公司分文件夹,结果面试前三天翻不到那道被问到的题。问题不在记忆力,在于收集方式从一开始就错了——你收集的是“题目文本”,不是“可检索、可分类、可验证的结构化数据”。用 Java 做这件事,核心就三件事:把散落在各处的面经变成统一格式,把题目按技术栈和难度打标,把答案和追问链补全。这套东西做出来,你面对的不再是几百道孤立的题,而是一个能按“JVM 内存模型”“Spring 循环依赖”“MySQL 索引失效”随时调取的本地知识库。适合谁?正在准备跳槽的 Java 后端,带新人需要题库的 Tech Lead,以及想把自己面试经历沉淀成资产的人。下面我从数据采集讲到检索优化,每一步都给能跑的代码和参数。
2. 从面经文本到结构化题目:采集与清洗的完整链路
2.1 为什么直接存 Markdown 是死路一条
很多人收集面经的第一步是新建一个面试题.md,把看到的内容粘进去。三个月后这个文件超过五千行,你想找“HashMap 扩容”相关的题,Ctrl+F 只能匹配到字面量,如果原文写的是“哈希表 resize”,你就漏了。更致命的是,你无法回答“我 Spring 相关的题准备到什么程度了”这种统计问题。
结构化收集的本质是把非结构化文本拆成字段。一道面试题至少需要这些字段:题干、技术栈标签、难度等级、出现频次、答案要点、追问方向、来源公司类型。用 Java 做这件事,我一般选 SQLite 做本地存储,零配置、单文件、支持全文索引,比 JSON 文件靠谱得多。下面这张表是我实际用的字段设计,你可以直接抄。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 自增主键 |
| question | TEXT | 题干原文,保留原始表述 |
| stack | TEXT | 技术栈标签,逗号分隔,如JVM,GC |
| difficulty | INTEGER | 1-5,1 为初级,5 为专家 |
| frequency | INTEGER | 被问次数,初始为 1 |
| answer | TEXT | 答案要点,支持 Markdown |
| followups | TEXT | 追问链,JSON 数组 |
| source_type | TEXT | 来源类型:大厂/中厂/外包/自研 |
| created_at | TEXT | 首次录入时间 |
| last_review | TEXT | 上次复习时间 |
注意:
stack字段不要用外键关联单独的表,面试题的技术栈交叉严重,一道题可能同时属于 JVM 和并发,用逗号分隔的文本配合 LIKE 查询在几千条量级下完全够用,过早规范化只会增加录入负担。
2.2 用 Java 写一个面经解析器:从复制文本到入库
假设你从某个面经帖子里复制了一段文本,格式大概是“1. 说说 JVM 内存模型?答:分为堆、栈、方法区……”这种。手写解析规则不现实,因为不同来源格式差异太大。我的做法是:先统一成“题干 + 答案”两段式,再用正则提取技术栈标签。
import java.sql.*; import java.util.regex.*; public class InterviewParser { // 匹配 "数字. 题干?答:答案" 或 "数字、题干?答案" private static final Pattern QA_PATTERN = Pattern.compile( "^\\d+[.、]\\s*(.+?[??])\\s*(?:答[::]\\s*)?(.+)$", Pattern.DOTALL ); // 技术栈关键词映射,用于自动打标 private static final String[][] STACK_KEYWORDS = { {"JVM", "内存|GC|垃圾回收|类加载|字节码|堆|栈"}, {"并发", "线程|锁|synchronized|volatile|CAS|AQS|线程池"}, {"Spring", "Bean|IOC|AOP|循环依赖|事务|SpringBoot"}, {"MySQL", "索引|事务隔离|MVCC|锁|explain|慢查询"}, {"Redis", "缓存|持久化|集群|过期|淘汰|分布式锁"}, {"消息队列", "Kafka|RocketMQ|RabbitMQ|消息|消费|积压"}, {"分布式", "一致性|CAP|分布式事务|注册中心|限流|熔断"}, {"网络", "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) { // 简单启发:题干长度 + 答案长度 + 是否含源码/底层关键词 int score = 1; if (q.length() > 30) score++; if (a.length() > 200) score++; if (q.matches(".*(源码|底层|原理|实现|设计).*")) score++; return Math.min(score, 5); } }这段代码的逻辑分三层。第一层是正则匹配,QA_PATTERN处理“数字+题干+答”的常见格式,DOTALL让答案可以跨行。第二层是技术栈自动打标,STACK_KEYWORDS里每个技术栈对应一组正则,命中就追加标签,这样你录入时不用手动选分类。第三层是难度估算,用题干长度、答案长度和关键词做启发式打分,虽然粗糙,但比全部默认 3 分强,后续复习时可以按难度筛选。
参数说明:sourceType建议用固定枚举值,比如BIG_TECH、MID_TECH、OUTSOURCE、SELF_DEV,方便后续按来源统计。frequency初始为 1,后续遇到相同题目时用UPDATE questions SET frequency = frequency + 1 WHERE question = ?累加。数据库文件interview.db放在项目根目录,记得加.gitignore,别把个人题库提交到公开仓库。
2.3 去重与合并:同一道题被问了八次怎么处理
面经收集到一定量后,重复题会大量出现。比如“HashMap 底层原理”可能以“说说 HashMap”“HashMap 怎么扩容”“HashMap 线程安全吗”三种表述出现。完全靠字符串相等去重会漏掉变体,靠语义相似度又太重。我的做法是两级去重:第一级用题干归一化后的哈希,第二级用技术栈+关键词重叠度。
import java.security.MessageDigest; import java.util.*; public class Deduplicator { // 归一化:去掉标点、空格、语气词,转小写 public static String normalize(String question) { return question.replaceAll("[??。,,!!、\\s]", "") .replaceAll("(说说|讲讲|谈谈|了解|知道|会吗|是什么|为什么)", "") .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); } } // 关键词重叠度,用于判断两道题是否在问同一件事 public static double overlap(String q1, String q2) { Set<String> s1 = new HashSet<>(Arrays.asList(q1.split("[\\s,,]"))); Set<String> s2 = new HashSet<>(Arrays.asList(q2.split("[\\s,,]"))); if (s1.isEmpty() || s2.isEmpty()) return 0; Set<String> intersection = new HashSet<>(s1); intersection.retainAll(s2); return (double) intersection.size() / Math.min(s1.size(), s2.size()); } }归一化那一步是关键。normalize方法把“说说 HashMap 的底层原理?”变成“hashmap底层原理”,这样不同语气词的变体就能命中同一个哈希。但光靠哈希不够,因为“HashMap 扩容”和“HashMap 原理”归一化后不同,实际是包含关系。所以第二级用overlap算关键词重叠度,超过 0.6 就认为是同一道题的变体,合并时保留题干更完整的那条,frequency累加。
注意:去重不要追求 100% 准确,宁可漏合并也不要错合并。把“JVM 内存模型”和“JVM 垃圾回收”合并成一道题,比保留两道独立的题危害大得多。我的经验是 overlap 阈值设 0.6 到 0.7 之间,低于 0.6 的变体手动确认。
3. 给题目打标签与建索引:让检索从分钟级降到秒级
3.1 技术栈标签体系怎么设计才不混乱
自动打标只能解决 70% 的场景,剩下 30% 需要手动补。但手动补的前提是标签体系本身不能太细。我见过有人把标签分成“JVM-内存-JMM-可见性”四级,结果录入时自己都记不住该选哪个。我的建议是两级标签:一级是技术域,二级是具体知识点,但二级标签不强制填,用题干里的关键词自然覆盖。
一级标签固定为这些:Java基础、JVM、并发、Spring、MySQL、Redis、消息队列、分布式、网络、算法、设计模式、系统设计、通用。每个一级标签下可以有关键词列表,用于自动匹配。比如JVM的关键词包括“内存模型、GC、垃圾回收、类加载、字节码、堆、栈、方法区、JMM、happens-before”。这套体系的好处是,你复习时可以按域推进,不会陷入细节。
-- 创建全文索引,支持题干和答案的模糊检索 CREATE VIRTUAL TABLE questions_fts USING fts5( question, answer, stack, content='questions', content_rowid='id' ); -- 触发器:插入时同步到 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 相关且答案里提到“可达性分析”的题 SELECT q.* FROM questions q JOIN questions_fts f ON q.id = f.rowid WHERE questions_fts MATCH 'stack:JVM AND answer:可达性分析' ORDER BY q.frequency DESC;FTS5 是 SQLite 的全文索引模块,比 LIKE 快一个数量级。content='questions'表示外部内容表模式,不重复存储数据。触发器保证插入时自动同步索引。查询时MATCH支持列过滤和布尔组合,stack:JVM限定技术栈,answer:可达性分析限定答案内容。按frequency降序排列,高频题优先看。
3.2 用 Java 封装检索接口:按技术栈+难度+频次组合查询
直接写 SQL 查询对使用者不友好,封装一个 Java 方法,输入技术栈、难度范围、关键词,返回排序后的题目列表。
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 List<Question> 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 1=1" ); List<Object> 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(); List<Question> 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方法的参数设计对应四种复习场景。只传stack是“我要过一遍 JVM 的题”;传minDiff=4, maxDiff=5是“只看高级题”;传keyword是“找和可达性分析相关的题”;三个都传就是精确打击。LIMIT 50防止一次返回太多,复习时看不过来。ORDER BY frequency DESC让高频题排前面,这是最实用的排序策略——被问得多的题,你大概率还会遇到。
注意:
stack LIKE '%JVM%'在数据量超过一万条时会变慢,如果题库很大,建议把stack字段拆成关联表,或者用 FTS5 的stack:JVM语法替代 LIKE。几千条以内 LIKE 完全够用,不要过早优化。
3.3 答案与追问链的补全策略
收集题干只是第一步,真正决定面试表现的是答案质量和追问深度。我的做法是:每道题录入时,答案字段只写“要点骨架”,不写完整段落。比如“JVM 内存模型”的答案骨架是“堆、栈、方法区、程序计数器、本地方法栈 + 各区域作用 + 可能抛出的异常”。追问链单独存 JSON,记录“如果面试官接着问什么,我该怎么答”。
// 追问链的 JSON 结构示例 String followups = """ [ {"q": "堆和栈的区别?", "a": "堆线程共享,栈线程私有;堆存对象,栈存局部变量和引用"}, {"q": "方法区在 JDK8 之后有什么变化?", "a": "永久代被元空间替代,元空间使用本地内存"}, {"q": "什么情况下会 OOM?", "a": "堆 OOM、栈溢出、元空间 OOM、直接内存 OOM"} ] """;这种结构的价值在于,你复习时不是背一道题,而是背一条追问链。面试官问“JVM 内存模型”,你答完基础,他大概率会追问“堆和栈的区别”,你提前准备好了,节奏就在你手里。追问链的 JSON 可以直接存 TEXT 字段,查询时用 Java 的 JSON 库解析,不需要额外建表。
4. 避坑与排查:题库建设中最容易翻车的五个点
4.1 现象:录入时自动打标准确率越来越低
原因:STACK_KEYWORDS里的正则太宽泛,比如“锁”这个关键词同时命中“并发”和“MySQL”,导致一道 MySQL 锁的题被打上并发标签。解决:给关键词加权重,或者用更精确的短语。比如把“锁”改成“synchronized|ReentrantLock|AQS|CAS”用于并发,MySQL 的锁用“行锁|表锁|间隙锁|MVCC”。定期抽查自动打标结果,发现误标就收紧正则。
4.2 现象:FTS5 查询报no such column: stack
原因:FTS5 外部内容表模式下,MATCH的列名必须和 FTS 表定义的列一致。如果questions_fts只定义了question, answer, stack,但查询时写了stack:JVM,而stack列在 FTS 表里存在,通常不会报这个错。真正的原因是触发器没建,FTS 表里没数据,或者content='questions'写成了content=questions。解决:先SELECT count(*) FROM questions_fts确认索引有数据,再检查触发器是否生效。
4.3 现象:去重时把不同难度的题合并了
原因:overlap只看关键词重叠,不看难度。一道“HashMap 原理”难度 2,一道“HashMap 扩容死循环”难度 4,关键词重叠度高,被合并成一道,结果复习时漏掉了高难度题。解决:去重时增加难度差判断,Math.abs(diff1 - diff2) > 1就不合并。或者合并时保留两条记录,用parent_id关联,查询时按难度展开。
4.4 现象:答案字段越写越长,检索变慢
原因:把完整文章粘进answer字段,单条记录超过 10KB,FTS5 索引膨胀。解决:答案只存要点,完整内容存到单独的文件,数据库里存文件路径。或者用answer_summary和answer_detail两个字段,检索用 summary,复习时按需加载 detail。
4.5 现象:复习时不知道该看哪些题
原因:没有复习状态跟踪。last_review字段录入了但没用起来。解决:加一个review_count字段,每次复习后加一,查询时按last_review升序排列,优先看最久没复习的题。再配合frequency加权,高频且久未复习的题排最前。
-- 复习优先级排序:高频 + 久未复习 + 高难度 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让高频题权重翻倍,difficulty让高难度题靠前,julianday差值让久未复习的题优先。last_review为空的新题不参与排序,单独用WHERE last_review IS NULL查出来先过一遍。
5. 把题库变成面试模拟器:一个可验证的进阶技巧
题库建好之后,最有效的验证方式不是“看一遍”,而是“讲一遍”。我给自己写了一个简单的模拟器:随机抽 10 道题,按技术栈分组,要求自己在不看答案的情况下口述,然后用录音转文字对比答案要点。这个过程中我发现,很多题“看的时候会”,“讲的时候卡”。卡住的地方就是真正的薄弱点。
具体做法是用 Java 写一个MockInterview类,从数据库按技术栈权重抽题。权重规则:frequency越高权重越大,last_review越久权重越大,difficulty在 3 到 4 之间的题权重最高(太简单的没必要练,太难的面试中很少被问到)。
public class MockInterview { public static List<Question> 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 ? """; // 实际使用时加随机因子,避免每次抽同样的题 try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, count * 3); // 先取三倍候选,再随机选 ResultSet rs = ps.executeQuery(); List<Question> 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())); } } }抽题之后,我会用手机录音,口述每道题的答案,然后回放对照数据库里的答案要点。这个过程很痛苦,但效果比反复看强得多。口述时你会发现,有些题你只能说出关键词,串不成句子;有些追问链你只记得第一层,第二层就断了。这些就是需要重点补的地方。
另一个验证方法是“交叉出题”:把 A 技术栈的题用 B 技术栈的视角回答。比如“Redis 持久化”这道题,试着从“如果让你设计一个持久化机制”的角度讲,而不是背 RDB 和 AOF 的区别。这种练法能暴露你对原理的理解深度,面试官稍微换个问法你就不行了。
注意:模拟器抽题后不要立刻看答案。先口述,再对照,最后把差异点补进
answer字段。这个循环跑上五轮,你的答案要点会越来越精炼,追问链会越来越完整。
我自己的习惯是每周跑一次模拟,每次 10 道题,录音存到本地,一个月后回听第一周的录音,能明显感觉到表达流畅度的变化。题库的价值不在于收集了多少题,而在于你能否在压力下把每道题讲清楚。这套 Java 实现的收集、清洗、索引、模拟链路,从第一行代码到能跑模拟,我用了两个周末,之后每次跳槽前只需要更新面经和跑模拟,不用再从头整理。希望帮到你。
本文还有配套的精品资源,点击获取