资讯详情

资讯详情

金三银四Java面试高频题盘点:原理、误区与答题框架

又是一年金三银四后台私信里问Java面试题的密度明显上来了。我翻了一下某技术社区最新的Java面试题总结结合最近帮朋友们做模拟面试的反馈整理了一份现阶段最值得投入时间的高频题清单。这篇文章不是简单地罗列题目和答案而是把每道题背后的考察意图、答题框架、常见误区和延伸追问都拆开讲清楚。无论你是准备跳槽的熟手还是刚开始找工作的应届生按着这份提纲往深里挖比我见过的大多数“面试题背诵手册”都要管用。先说一个很多人忽略的事实现在的Java面试早就不是“背八股”能过关的了。面试官普遍会通过一道基础题连环追问考察你到底是真的理解还是只记住了结论。所以这篇文章在写每个知识点的时候都有意识地补充了“为什么是这样”和“换个场景会怎样”这两个维度帮你把知识串成网而不是堆成一座座孤岛。1. Java基础与集合框架高频题1.1 String、equals和hashCode的连环坑Java基础这部分面试官最爱从String切开。第一问通常是“String为什么是不可变的”很多人只答得上“因为有final修饰”但这只是表象。更核心的原因有三个字符串常量池需要共享对象如果可变的话一个引用改了值其他引用全部跟着变String经常充当HashMap的key可变对象做key会导致hashCode变化再也查不到原来的value还有安全层面的考虑比如网络连接参数、类加载路径都用String表示不可变可以防止被恶意篡改。紧接着第二问就来了“new String(abc)和abc的区别是什么”。这里要分两层答。第一层是对象数量直接写abc时如果常量池里没有就创建一个常量池对象new String(abc)则在常量池之外额外创建一个堆对象。第二层是引用地址字面量赋值指向常量池new出来的指向堆内存用比较结果必然不同。面试官通常会追问“那equals呢”这时候就要把equals和的关系说清楚比较的是引用地址equals如果没被重写默认也是比较地址但String重写了equals逐个字符比较内容。这里有个特别容易踩的坑重写equals必须同时重写hashCode。我见过不少候选人能把两条规则倒背如流但追问“为什么”就卡住了。道理其实很简单HashMap的存取过程是“先找桶再比内容”定位桶靠的是hashCode桶内比对靠的是equals。如果两个相等的对象hashCode不同散列到不同的桶里HashMap就认为它们不存在get的时候永远返回null。面试官如果再往深处问还会让你分析“为什么重写equals时hashCode要保证一致性”答案就是基于这个查找机制相等的对象必须有相同的hashCode否则基于哈希的集合全部失效。1.2 HashMap的扩容与红黑树不只是背参数集合框架的面试重头戏基本都压在HashMap身上。最基础的问题是“HashMap的底层结构是什么”JDK 1.8之后的答案应该是数组加链表加红黑树。链表长度超过8且数组长度达到64时链表转红黑树红黑树的节点数降到6时退化为链表。这里有两个数字容易记混为什么阈值是8而不是9又是为什么不是10官方的解释是基于泊松分布在负载因子0.75、随机哈希的情况下链表长度达到8的概率已经低到千万分之一用8作为阈值是空间和时间的平衡点。至于为什么退化阈值是6是为了避免链表和红黑树在边界值附近频繁转换留出缓冲区间。接下来必然会问扩容机制。HashMap默认初始容量16负载因子0.75当元素个数超过容量乘以负载因子时触发扩容容量翻倍。这里面最值得展开的是“为什么负载因子是0.75”以及“为什么容量总是2的幂次方”。负载因子0.75是空间利用率和查询效率的折中太高则冲突剧增太低则浪费空间。容量设计为2的幂核心是为了让(n - 1) hash代替取模运算位运算比取模快得多而且只有当n是2的幂时hash (n - 1)才等价于hash % n。还有一个细节是hash值的处理高16位和低16位做异或运算目的是让高位也参与散列降低冲突概率。HashMap的线程安全问题也是必问点。JDK 1.7在并发扩容时可能出现环形链表导致get死循环JDK 1.8改进了扩容方式避免了环链问题但数据丢失和size不准确的问题依然存在。所以并发场景不能直接用HashMap要么用ConcurrentHashMap要么用Collections.synchronizedMap包装。这个追问一般会自然过渡到并发编程话题正好接上下一节。2. 并发编程核心考点与答题框架2.1 synchronized的锁升级过程别只说“有锁”并发编程是Java面试的深水区也是区分度和含金量最高的一块。synchronized是第一个必考题现在再答“synchronized是重量级锁性能差”这种话就露馅了因为JDK 1.6之后做了大量锁优化锁是有状态的并非一开始就是重量级。正确的回答框架是锁升级路径无锁 - 偏向锁 - 轻量级锁 - 重量级锁。偏向锁解决的是“只有一个线程访问同步块”的场景锁会记录第一个获取它的线程ID后续这个线程再来时无需任何CAS操作直接进入。如果有另一个线程竞争偏向锁撤销并升级为轻量级锁。轻量级锁通过CAS自旋来获取锁适合线程交替执行、同步块执行很快的场景。如果自旋超过一定次数仍拿不到锁或者竞争线程数过多就会膨胀为重量级锁依赖操作系统互斥量线程阻塞和唤醒涉及用户态与内核态切换开销最大。面试官接下来大概率会问“锁对象是什么”。这里要区分三种用法修饰实例方法时锁的是当前实例对象修饰静态方法时锁的是Class对象修饰代码块时锁的是括号里指定的对象。很多人在这里栽跟头是没搞清楚“锁的是对象不是代码”所以两个线程如果锁的是不同的对象实例它们之间完全不互斥这是一个非常经典的并发bug来源。再往深处问就是“synchronized和ReentrantLock怎么选”。从功能上看ReentrantLock支持公平锁、可中断获取锁、超时获取锁、多个条件队列synchronized在JDK 1.8之前都不支持这些。但从JDK 1.6之后synchronized经过优化性能上与ReentrantLock差距不大而且synchronized是JVM原生支持的使用更简单不会忘记释放锁。我的建议是能用synchronized就用synchronized只有需要超时控制、可中断等高级特性时才上ReentrantLock。2.2 volatile与JMM从底层可见性说起volatile是并发编程里另一个高频考点而且面试官特别喜欢把它和JMMJava内存模型放在一起问。核心机制是两条保证可见性和禁止指令重排序。可见性指的是一个线程修改了共享变量其他线程能立刻看到原理是volatile变量在写操作时会强制刷新主内存读操作时从主内存重新加载同时会使其他CPU核心的高速缓存中对应的缓存行失效。禁止重排序则是通过内存屏障实现的volatile写操作前插入StoreStore屏障写操作后插入StoreLoad屏障volatile读操作后插入LoadLoad和LoadStore屏障。这里有个概念必须和synchronized区分开volatile不保证原子性。最经典的例子是count即使count被volatile修饰两个线程同时执行这个操作依然可能丢更新因为“读取-修改-写入”这个复合操作不是原子的。所以volatile的适用场景是一个线程写、多个线程读的状态标志或者作为保证可见性的触发器。面试官如果继续深入会问“指令重排序是怎么发生的”。现代CPU和编译器为了提升指令级并行度会在不影响单线程执行结果的前提下调整指令顺序。但在多线程环境下重排序可能导致意想不到的结果。最典型的例子是双重检查锁单例如果instance字段没有用volatile修饰在instance new Singleton()这行代码里“分配内存、初始化对象、把引用赋值给变量”三步可能被重排序为“分配内存、把引用赋值给变量、初始化对象”另一个线程就可能拿到一个未初始化完成的对象。加了volatile之后通过内存屏障禁止了这个重排序问题才被根治。2.3 线程池的七个参数现场推演拒绝策略线程池是Java并发面试的压轴题基本必考。核心是ThreadPoolExecutor的七个参数核心线程数、最大线程数、空闲线程存活时间、时间单位、阻塞队列、线程工厂、拒绝策略。面试官会先问“线程池的执行流程”这个流程要按顺序说清楚先判断核心线程是否已满没满则创建核心线程执行任务满了则尝试放入阻塞队列队列满了则尝试创建非核心线程直到达到最大线程数最大线程数也满了执行拒绝策略。接下来是参数设置的考量。CPU密集型任务核心线程数可以设为CPU核数加一多出来的一个是为了防止某一线程因缺页中断或其他原因暂停时CPU空闲IO密集型任务由于线程经常阻塞等待IO可以设置更多线程常见的经验公式是CPU核数乘以二或者CPU核数的两倍加一。但实际工作中这些值必须结合压测结果调整不能死套公式。拒绝策略有四种必须能解释清楚每种策略的适用场景AbortPolicy直接抛出RejectedExecutionException这是默认策略也是最激进的CallerRunsPolicy由提交任务的线程自己执行该任务起到节流作用DiscardPolicy直接丢弃任务不抛异常DiscardOldestPolicy丢弃队列里最旧的任务再尝试提交当前任务。实际项目中我最常用的是CallerRunsPolicy因为它不会丢任务又能通过让调用线程执行任务来反向压慢提交速度。千万别用DiscardPolicy或DiscardOldestPolicy静默丢任务在线上是最难排查的问题之一。3. JVM内存与垃圾回收从题目到线上排查3.1 内存区域划分与对象生命周期JVM这块对后端开发来说属于“平时不用管、面试必被问、出了问题不会查”的尴尬地带。大部分题目其实是为了验证你有没有系统性地读过虚拟机规范。首先要把运行时数据区划分清楚堆、虚拟机栈、本地方法栈、方法区元空间、程序计数器。堆是所有线程共享的存放对象实例也是GC的主要区域虚拟机栈是线程私有的每个方法调用对应一个栈帧栈帧里包含局部变量表、操作数栈、动态链接、方法出口。这里容易搞混的是“栈上分配”和“TLAB”这两个概念。现代JVM对于不会被外部引用的对象如果满足逃逸分析条件可能直接在栈上分配方法结束即销毁无需GC介入TLAB则是线程本地分配缓冲区是堆内存中为每个线程划出的一块私有区域目的是减少多线程竞争堆内存分配时的锁冲突。对象在Eden区出生经过Minor GC后如果存活且年龄达到阈值默认15会晋升到老年代大对象也可能直接进入老年代。这个年龄阈值可以通过-XX:MaxTenuringThreshold调整但默认15是CMS和Parallel等收集器的通用值。面试经常会问“对象一定分配在堆上吗”答案是否定的需要分情况经过逃逸分析的对象可能在栈上分配或通过标量替换消除TLAB分配本质上还是在堆内。这个题考察的是对JIT编译器和内存分配的深入理解能答出逃逸分析的基本原理就已经超过大部分候选人了。3.2 垃圾收集器选型与调优参数垃圾回收的题目现在越来越务实面试官不会满足于让你背诵“复制算法、标记清除、标记整理”的定义而是会结合线上场景考收集器选型。JDK 1.8默认的Parallel Scavenge加Parallel Old追求的是吞吐量CMS追求的是低停顿但会产生内存碎片而且GC线程和业务线程并发执行时会占用CPU资源G1把堆分成多个Region通过维护一个优先级列表来跟踪每个Region的回收价值优先回收价值收益最大的Region兼顾了吞吐量和停顿时间。如果面试官让你“比较一下CMS和G1”可以从三个方面答。第一是回收范围CMS只能回收老年代需要配合年轻代收集器使用G1可以同时回收年轻代和老年代。第二是停顿控制CMS无法预测停顿时间G1则可以通过-XX:MaxGCPauseMillis设置目标停顿时间虽然这个值只是一个期望但G1会据此调整回收策略。第三是碎片问题CMS采用标记清除算法会产生碎片G1整体上采用标记整理加复制不会产生碎片。调优参数也是高频题。最常用的几个-Xms和-Xmx设置堆初始大小和最大大小生产环境建议两者设为相同值避免堆大小动态伸缩造成的性能波动-XX:NewRatio设置新生代和老年代的比例-XX:SurvivorRatio设置Eden区和Survivor区的比例默认8:1:1-XX:HeapDumpOnOutOfMemoryError在OOM时自动导出堆转储文件这个参数极其重要生产环境一定要开否则OOM后连排查线索都没有。值得一提的是线上排查的方法论。遇到CPU飙升先top -Hp定位线程再用jstack导出线程快照找RUNNABLE状态的业务线程遇到OOM先加-XX:HeapDumpOnOutOfMemoryError导出dump文件然后用MAT或JProfiler分析是堆内存泄漏还是内存溢出。内存泄漏的排查思路是从GC Roots出发做引用链分析找出哪个对象占用了大量内存且无法被回收。这些实战能力比背诵任何一个参数都重要面试官听到你能完整描述一次OOM排查流程加分效果是立竿见影的。4. Spring与MySQL/Redis逐一拆解4.1 Spring IOC与AOP从设计思想答题到了框架这块Spring几乎是Java后端岗位绕不开的必问题。IOC和AOP是Spring的两大基石。回答IOC时不能只说“控制反转就是把对象创建交给容器”要往深了说传统开发模式下对象之间的依赖关系由对象自身创建和维护导致高耦合IOC把创建和装配对象的控制权交给容器对象只声明依赖容器负责注入从而实现解耦。依赖注入有构造器注入、setter注入、注解注入三种方式Spring官方推荐构造器注入因为可以保证依赖不可变且不为空。AOP的回答框架要覆盖两层。第一层是概念切面、切点、通知、连接点四个核心概念要能讲清楚通知类型区分前置通知、后置通知、环绕通知、异常通知、最终通知。第二层是原理Spring AOP默认使用动态代理对于实现了接口的类使用JDK动态代理基于接口生成代理类对于没有实现接口的类使用CGLIB代理通过继承生成子类。这里有个经典面试陷阱基于JDK动态代理的类只能用接口类型接收代理对象如果直接用实现类类型接收会抛ClassCastException。另外一个必考细节是“为什么JDK动态代理要求目标类实现接口”因为Proxy.newProxyInstance生成的是接口的实现类没有接口就无法生成。事务也是重头戏最常问的是“事务失效的场景有哪些”。这个题考察的是实践经验我建议把几个典型场景牢牢记清楚数据库引擎不支持事务比如MySQL的MyISAM引擎方法不是public修饰的Spring事务默认基于AOP代理private方法无法被代理类内部自调用this调用本类方法不经过代理对象事务注解不生效方法内捕获异常且没有抛出事务无法感知传播行为配置错误比如REQUIRED和REQUIRES_NEW没区分开多线程调用事务管理器绑定的线程和子线程不是同一个。这几个场景里自调用和异常被吞是日常开发中最常见的坑一定要重点说。4.2 MySQL索引与事务隔离级别MySQL这块的题集中在索引、事务、锁三个方向。索引的底层数据结构必须答B树而且要说出为什么不用B树、红黑树、哈希表。哈希表适合等值查询但不支持范围查询红黑树在数据量大时树高度过高磁盘IO次数太多B树把所有数据都放在节点中每个节点能存储的key数量有限树依然偏高B树把数据都放在叶子节点叶子节点之间通过双向链表连接既支持范围查询又通过增加节点存储key的数量降低了树高减少了磁盘IO。关于索引失效的场景这是考察实战经验的经典题。最核心的原则是索引失效本质上是无法利用索引的有序性进行快速定位。典型场景包括对索引列使用函数或表达式隐式类型转换比如字符串索引列用数字去查模糊查询以通配符开头like %abc无法走索引like abc%则可以联合索引不满足最左前缀原则使用or连接时如果其中一个条件没有索引。了解这些场景之后还要能说出一个反直觉的点在联合索引中如果查询条件里第一个字段是范围查询后面的字段无法走索引但第一个字段本身可以走这个细节经常被误解。事务隔离级别和MVCC也是必聊的。四种隔离级别要能背得出并能举例说明读未提交、读已提交、可重复读、串行化。MySQL默认的隔离级别是可重复读这和其他数据库不太一样。在这个级别下通过MVCC多版本并发控制机制普通快照读实现了读写不互斥。MVCC的核心是三个隐藏字段DB_TRX_ID事务ID、DB_ROLL_PTR回滚指针、DB_ROW_ID行ID配合undo log实现版本链通过ReadView实现一致性快照。可重复读和读已提交的区别在于ReadView的生成时机前者在事务第一次执行select时生成ReadView后续一直复用后者每次select都生成新的ReadView所以能看到其他事务已提交的最新数据。4.3 Redis的数据结构与缓存一致性Redis相关的面试题数据结构是第一个入口。五种基本数据类型各有各的底层编码回答时最好提到底层实现以体现深度String可以用int、raw、embstr三种编码满足整数且长度小时用int短字符串用embstr减少内存分配Hash可以用ziplist和hashtable两种编码元素少且长度短时用ziplist省内存List由quicklist实现是双向链表和压缩列表的结合Set底层是intset或hashtableZSet底层是skiplist加hashtable。面试官如果追问“为什么ZSet用跳表而不是红黑树”答案可以简化为跳表实现更简单而且范围查询的操作复杂度同为O(logN)跳表的缓存友好性更好。缓存一致性问题是目前面试的重灾区因为设计到方案选型。最核心的场景是“更新数据库和删除缓存顺序怎么选”。我建议回答的框架是先说不推荐什么不推荐先更新数据库再更新缓存因为并发下两个线程的更新顺序可能导致缓存里是旧数据也不推荐先删除缓存再更新数据库因为删除缓存后、更新数据库前有读请求会把旧数据回填进缓存后续一直读到脏数据推荐先更新数据库再删除缓存配合缓存过期兜底这种方案在绝大多数场景下足够可靠。如果面试官继续追问“极端情况下缓存怎么保持最终一致”可以引出延迟双删或订阅binlog的异步删除方案。延迟双删就是在更新数据库后先删一次缓存短暂休眠几百毫秒再删第二次目的是把回填旧数据的那个读请求写入的缓存删掉。但这个方案引入了休眠等待用起来要谨慎。更稳妥的方式是用binlog订阅组件监听数据库变更变更后异步删除对应缓存把一致性维护从业务代码中抽离出去。这些方案的取舍和代价能讲清楚比单纯背结论有力得多。5. 系统设计与算法题最后的加分环节5.1 设计题从三个维度拆解到了系统设计题很多候选人开始发慌其实这类题并不是真让你在半小时内设计出一个完整系统而是考察你有没有一套稳定的分析框架。我习惯从三个维度拆解功能性需求、非功能性需求、核心难点。功能性需求就是系统要做什么比如设计一个短链系统核心功能是长链接转短链、短链跳转长链接非功能性需求是吞吐量、延迟、可用性、一致性要求核心难点则要找出最容易出问题的地方比如短链系统的哈希冲突、存储容量、跳转性能。短链系统是高频设计题解题思路比较固定生成短码时用发号器雪花算法或数据库自增ID或者对长链接做哈希再截断数据库存储长短链映射关系跳转时用301还是302需要权衡302是临时重定向适合需要统计点击量的场景301是永久重定向浏览器会缓存结果减少服务端压力。还有一个重点是短码的字符集选择62个字符大小写字母加数字是业界标配6位短码能表示的数量接近570亿完全够用。秒杀系统设计题也经常出现。核心难点就仨瞬时高并发、超卖问题、防刷问题。超卖的本质是库存扣减的原子性解决方案是数据库层面用UPDATE ... SET stock stock - 1 WHERE stock 0的乐观扣减或者用Redis的原子性decr操作。防刷可以通过接口限流、用户维度频率控制、验证码等手段配合。记忆点在于秒杀系统要保护的是数据库所以要在前端、网关、Redis缓存、消息队列层层挡流量最终打到数据库的请求已经很少了。答到这个层次的候选人面试官通常会很满意。5.2 手写算法的准备策略最后一个环节是算法题。很多Java面试者觉得算法题是拦路虎其实准备策略对了投入产出比很高。按出现频率排序最值得练的是四大类二叉树相关层序遍历、最近公共祖先、路径和、动态规划入门爬楼梯、打家劫舍、最长上升子序列、双指针和滑动窗口两数之和、无重复字符的最长子串、排序和TopK问题快排、堆排、快选。TopK问题要能手写两种解法基于快排partition的O(n)平均解法以及基于小顶堆的O(nlogK)解法后者在海量数据场景下更实用。面试时写算法有一些容易被忽视的细节。先确认输入输出约束是不是有可能为空、是不是可能超大不要闷头写代码先说思路再动手很多面试官更看重思考过程写完主动测试边界用例比如空数组、单元素数组、全相同元素数组。这些看起来是小习惯但在面试现场却能体现工程素养甚至能弥补一点解题速度的不足。6. 针对金三银四的复习节奏建议聊完具体知识点最后说点复习方法。金三银四的窗口期看着有两个月实际上很多公司的面试流程从三月初就密集启动了所以复习必须有节奏感。我的建议是分成三轮第一轮用一周左右过高频基础题按照本文提到的模块逐个扫雷目标是每个知识点都能用自己的话讲明白第二轮用两周左右做专题强化针对你目标岗位的JD里明确提到的技术栈深挖比如岗位要求熟悉高并发就把并发编程和Redis的题目横向做一遍第三轮是模拟面试找朋友互相提问或者对着录音自己讲重点练表达的逻辑性和时间控制。还有两个特别值得强调的复习策略。一个是建立“题目-原理-场景”的关联。单纯背答案最容易被连环追问打穿但如果每道题都追问自己“这个设计解决的是什么问题、不这样会怎样、换个场景还适用吗”知识就能真正内化成能力。另一个是输出倒逼输入尝试把某个技术点写成一篇文章或讲给身边的人听讲不顺畅的地方就是还没掌握透的地方这种查漏补缺方式比多刷一百道题都有效。我自己每次准备面试都会做一件事把复习过程中遇到的模糊点记在一个单独的文件里每天晚上睡前过一遍。这个方法看起来笨但很管用——模糊点往往是面试官最喜欢深挖的地方你提前把它们消灭掉现场就不会慌了。最后再分享一个我踩过的坑别在简历上写“熟悉”一个技术但被问到细节时只能说出一两个名词。面试官对这类“熟悉”的容忍度非常低。宁可写“了解”并确保能深度展开也不要写“熟悉”然后支支吾吾。简历上的每一个词都要做好被当场检验的准备。祝各位准备跳槽的朋友都能在这个春天拿到满意的offer。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →