ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

金三银四Java基础面试:十道题拆解面试官筛选逻辑

金三银四Java基础面试:十道题拆解面试官筛选逻辑 金三银四又到了。这段时间社群里问得最频繁的除了简历怎么写就是 Java 基础面试题怎么准备。我在每年这个时候都会帮身边的朋友和读者做模拟面试一个月下来差不多二十场面得多了就发现一个现象很多候选人不是不会而是完全不知道面试官抛出基础八股文的时候脑子里真正在衡量什么。这篇文章把我常用来做第一轮筛选的十道 Java 基础题拆开来讲每组题对应一类能力面向对象考察的是抽象能力集合框架考察的是源码意识JVM 和并发考察的是底层原理掌握程度。每道题我会给一个可以直接照着练的回答思路也会把模拟面试里高频出现的错误回答列出来帮你避开那些一开口就扣分的坑。适合谁看正在准备 Java 基础面试的应届生和社招候选人以及想自查基础有没有漏洞的开发。如果你已经能把每个问题背后的“为什么”说出来这篇可以帮你检查表达节奏如果你还停留在“知道概念但说不深”的阶段按我下面的分组去补效率会高很多。1. 金三银四Java基础面试十道题背后的筛选逻辑先把话放在前面别把基础八股文当成“背了就完事”的东西。面试官问基础题表面上是考知识点实际上是在看三件事——你能不能把复杂问题讲清楚、你知不知道知识点的边界、你被连续追问的时候是慌着改口还是能顺着思路继续深入。这三点恰恰是平时写代码、做 Code Review、带新人的时候最重要的能力。框架可以换语言特性可以忘但面向对象、集合、JVM、并发这些地基要是松的项目里遇到稍微刁钻一点的 bug就会无从下手。1.1 基础八股文能筛出什么一个“精通Java”候选人翻车的现场先讲个真实场景。上周模拟面试候选人简历上写着“精通 Java”我问他 String 为什么设计成不可变他愣了几秒说“因为 String 类有 final 修饰”。这个回答不算错但明显没有想过更深一层不可变能带来什么好处跟字符串常量池有什么关系跟线程安全又有什么关系通常到这一步面试官心里已经能判断候选人的深度了。基础题不是看你会不会背定义而是看你能不能把一个点讲透。十个点里面有三四个能讲到源码级或原理级面试官就会认为你平时写代码是带思考的。反过来如果每个问题都只能说出结论、说不了原因在面试官眼里和“没准备过”区别不大。还有一个更现实的原因后端项目里大量代码都在跟集合、字符串、线程池打交道基础不牢的人写出问题代码的概率更高面试官宁可要一个“基础扎实但框架不熟”的人也不太敢要“框架用得飞起但一问原理就懵”的人。1.2 十道题的考点地图别孤立地背要成体系先整体看一下这十道题的分布我再逐个展开。这张表是给我自己模拟面试用的自测清单你也可以照着它自查。题号考察点常见追问方向1面向对象三大特性多态的实现原理、重载和重写的区别2与equals、hashCode重写equals为何必须重写hashCode3String不可变性StringBuilder与StringBuffer区别4ArrayList与LinkedList扩容机制、插入删除复杂度5HashMap底层原理1.7到1.8的改动、为什么线程不安全6ConcurrentHashMap锁粒度演进、CAS与synchronized7JVM内存区域哪些内存会OOM、对象分配与晋升8类加载与双亲委派如何打破双亲委派、SPI场景9线程池核心参数任务执行流程、拒绝策略10volatile与原子性可见性、有序性、DCL单例这张表不是让各位背题号而是想说一件事这些题之间是有联系的。比如第 5 题 HashMap 和第 6 题 ConcurrentHashMap 放在一起看就是“并发场景下如何用更细的锁粒度提升性能”第 9 题线程池和第 10 题 volatile 放在一起看就是“并发编程到底在解决哪些问题”。按组准备比一道题一道题孤立去背要有效得多。2. 第一梯队面向对象与对象判等代码之外的考察点第一梯队的三道题面试官通常放在前五分钟。它们难度不大但最容易暴露“只会背定义”的问题所以我会把回答重心放在“怎么落到代码上”。2.1 问题一面向对象三大特性怎么答出代码感面试官经常这样问“说一下面向对象的三大特性。”如果你直接说“封装、继承、多态”然后开始背概念这题基本就是及格分。要拿高分得把每个特性落到代码上让面试官觉得你平时真的在写代码而不是在背教材。我的回答思路是分三点展开。封装核心是字段用 private 修饰对外只暴露必要的 public 方法本质是隐藏内部实现控制外部修改的边界。比如订单类里的状态字段不直接暴露 setter而是提供 cancelOrder 这样的业务方法来控制状态流转这就是封装在保护业务规则。继承是子类复用父类代码表达 is-a 关系但要补一句“组合优先于继承”因为继承层级过深会导致代码改起来牵一发动全身。多态是父类引用指向子类对象调用方法时根据实际类型分派重载是编译期多态重写是运行期多态。为了加深印象我通常会现场写一小段代码来说明多态class Animal { public void speak() { System.out.println(Animal speak); } } class Dog extends Animal { Override public void speak() { System.out.println(Dog bark); } } Animal a new Dog(); a.speak(); // 输出 Dog bark然后补一句“运行期多态依赖 JVM 的方法表invokevirtual 指令在运行时从方法表里找实际类型的方法入口所以也叫动态绑定”。这道题最容易翻车的追问是“重写和重载除了参数列表和返回类型底层有什么不同”能答出“方法重载在编译阶段就能确定调用哪个版本方法重写需要等到运行时通过方法表分派”说明你是真的懂而不是背了个表格。2.2 问题二与equals以及hashCode这个送命题这题几乎是 Java 基础面试的保留节目回答的关键是分清楚两个层面。第一层 在比较基本类型时比较值在比较引用类型时比较内存地址。equals 是 Object 类的方法Object 默认实现等价于 但子类可以重写。String 的 equals 被重写为比较字符内容所以abc.equals(new String(abc))为 true但用 比较是 false。第二层也是容易被追问的部分为什么重写 equals 一定要重写 hashCode。我一般用 HashMap 举例HashMap put 的时候先计算 key 的 hashCode 定位桶再用 equals 判断桶内是否已有相同 key。如果两个对象 equals 相等但 hashCode 不同它们会被放进不同桶导致同一个 key 在 map 里出现两份get 的时候又可能 get 到 null逻辑直接崩掉。面试官再往下追会问“重写 equals 时要注意哪些约定”。可以答出对称性、传递性、一致性以及 getClass 判断和 instanceof 判断的区别。比如Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; Person person (Person) o; return age person.age Objects.equals(name, person.name); } Override public int hashCode() { return Objects.hash(name, age); }getClass 判断更严格子类对象不会被判断为相等instanceof 则允许子类视为同一类型但容易破坏对称性。实际业务里用 getClass 更安全。这里还要提一句 hashCode 的三个约定equals 相等hashCode 必须相等hashCode 相等equals 不一定相等hashCode 不相等equals 一定不相等。把这三个约定说出来面试官会觉得你的记忆是有结构的。我在模拟面试里遇到过一个反面例子候选人重写了 equals 但没重写 hashCode还振振有词说“业务里没用到 HashSet 所以没事”。这种回答很危险一旦面试官追问“如果将来放进 HashMap 呢”局面就很难圆回来。所以我的建议是只要你重写了 equals就顺手重写 hashCode这是习惯不是表演。2.3 问题三String为什么不可变StringBuilder和StringBuffer怎么选String 不可变性这个点要分三层来答。第一层机制层String 内部用 private final char value[]JDK9 之后是 byte[]存储字符String 类本身被 final 修饰且没有暴露任何能修改内部数组的方法。所以对象一旦创建内容就固定了。第二层作用层不可变能带来三个好处一是字符串常量池可以复用字符串对象如果 String 可变池里的引用就会互相影响二是不用考虑线程安全问题三是字符串的 hashCode 可以安全缓存提升 HashMap 等场景的效率。第三层对比层既然 String 不可变频繁拼接字符串就要用 StringBuilder。StringBuilder 本身不保证线程安全单线程下性能最好StringBuffer 在每个修改方法上加了 synchronized线程安全但性能略低。现代开发中局部变量拼接场景基本没有多线程竞争优先用 StringBuilder。这里可以补一个字符串常量池的例子直接量赋值String s1 abc会优先从常量池取new String(abc)则会在堆中创建一个新对象同时如果常量池里还没有“abc”会在常量池里也创建一个字面量对象。很多人以为 new String 一定创建两个对象其实准确说法是“可能创建一个或两个对象”取决于常量池是否已有该字面量。常见追问是“循环里用 拼接字符串编译器会做什么”要说明单个 在编译期会被优化成 StringBuilder但在循环体内每次循环都会 new 一个 StringBuilder导致创建大量临时对象。所以循环拼接必须手动用 StringBuilder。这个点既能体现性能意识也能体现对编译原理的敏感。3. 第二梯队集合框架与并发容器从源码到面试官想听的话集合框架属于“看似简单、实际最能拉开差距”的部分。面试官问 ArrayList 和 HashMap重点不是听你报数据结构而是看你对源码细节有没有概念。3.1 问题四ArrayList和LinkedList别再说“数组快链表慢”这类题目最怕的就是“背诵式对比”ArrayList 底层是数组查询快增删慢LinkedList 底层是链表增删快查询慢。这么说面试官大概率会追问“那 LinkedList 在中间插入快吗”事实上LinkedList 在中间插入时先要找到插入位置而查找是 O(n) 的所以中间插入整体并不比 ArrayList 快多少。ArrayList 的中间插入慢在 System.arraycopy 移动元素但后半部分元素的移动是内存级别的批量复制实际性能往往没有想象中那么差。所以回答时要补一句“LinkedList 的优势主要体现在大量头部或尾部的插入删除场景以及需要频繁操作 Iterator 时”。ArrayList 的扩容机制也是必问点默认初始容量 10每次扩容到原来的 1.5 倍也就是oldCapacity (oldCapacity 1)通过Arrays.copyOf复制到一个新数组。这里可以顺便说说为什么扩容后复制会很耗性能以及预估容量时用new ArrayList(expectedSize)减少扩容次数。int oldCapacity elementData.length; int newCapacity oldCapacity (oldCapacity 1); // 1.5倍 elementData Arrays.copyOf(elementData, newCapacity);面试官如果追问“为什么不用 2 倍而用 1.5 倍”可以回答扩容倍数太大会浪费空间太小会导致频繁扩容和复制1.5 倍是空间和时间之间的折中也是 Java 官方多年实践下来的选择。这个细节能回答出来说明你不是只看了一篇博客是真的翻过源码。3.2 问题五HashMap底层原理为什么1.7到1.8是分水岭HashMap 这题几乎没有人不背但能答到什么深度差别很大。基础版回答JDK1.8 的 HashMap 是数组链表红黑树。put 时先计算 key 的 hash通过(n-1)hash定位桶。如果桶是空的直接放 Node如果桶不为空用链地址法解决哈希冲突。当链表长度达到 8 且数组长度达到 64 时链表转成红黑树把查询复杂度从 O(n) 降到 O(log n)。JDK1.7 只有数组链表用的是头插法JDK1.8 改成了尾插法。进阶版要能回答三个“为什么”。第一个为什么为什么树化阈值是 8因为加载因子 0.75 下桶内节点数达到 8 的概率极低这个值是根据泊松分布算出来的8 这个阈值是时间和空间的平衡点树节点比链表节点占用更多内存转换代价比较高。第二个为什么为什么数组容量要求是 2 的幂因为定位桶用的是(n-1)hashn 是 2 的幂时这个位运算等价于hash % n而且更快同时扩容时可以用e.hash oldCap快速判断元素是老位置还是“老位置oldCap”。第三个为什么为什么线程不安全JDK1.7 并发 put 时扩容用头插法可能形成环形链表导致 get 死循环JDK1.8 改成尾插法解决了这个循环问题但并发 put 仍然可能互相覆盖出现数据丢失。这里建议准备一段话“JDK1.8 在扩容时如果e.hash oldCap为 0元素留在原位置结果为 1元素移动到原索引 oldCap 的位置。因为容量翻倍后多出来的最高位正好决定了 hash 值对应下标的差异。”能说出这个细节面试官通常会对你的源码阅读能力留下印象。3.3 问题六ConcurrentHashMap如何把锁粒度越做越细这题通常紧跟 HashMap考察的是并发容器设计思路。面试官想听的不是“它线程安全”这个结论而是“它是怎么做到线程安全又保持性能的”。JDK1.7 的实现是 Segment 分段锁整个 Map 被分成默认 16 个 Segment每个 Segment 里面是一个小 HashMapput 时只需要锁住对应的 Segment不同 Segment 之间可以并发写。这样锁粒度比直接锁整个 Map 细了很多但 Segment 数量固定扩容时还是整个 Segment 内的数组扩容。JDK1.8 的实现变化很大直接用 Node 数组 synchronized CAS不再有 Segment。put 的时候如果目标桶是空的用 CAS 直接插入不需要加锁如果桶不为空就对桶的头节点加 synchronized。锁粒度从“一个 Segment 管一段桶”细到“一个桶一把锁”并发度更高。面试官如果追问“为什么 1.8 用 synchronized 而不是 ReentrantLock”可以从两个角度答。一是 synchronized 在后续 JDK 版本里做了锁升级优化偏向锁、轻量级锁、重量级锁竞争不激烈时性能并不差二是 ConcurrentHashMap 已经把锁粒度控制到桶级别持锁时间很短复杂锁机制带来的收益已经不大了synchronized 实现更简洁。再补一句“CAS 适合冲突少的场景冲突多时 CAS 自旋反而消耗 CPU所以桶不为空时退化为 synchronized”这题基本就稳了。4. 第三梯队JVM与并发基石答出“为什么”才算过关到了 JVM 和类加载这块一般不是初级面试的主战场但 Java 基础八股文中一定会出现。面试官用这部分题来区分“会用框架的人”和“理解 Java 运行机制的人”。4.1 问题七JVM内存区域哪些会OOM要分清楚这题不能只背名字最好按“线程私有/线程共享”这个维度来组织然后每个区域都给到一个可触发的异常。区域线程私有/共享作用可能异常程序计数器私有当前线程执行的字节码行号指示器不会OOM虚拟机栈私有每个方法调用的栈帧StackOverflowError / OOM本地方法栈私有native方法调用StackOverflowError / OOM堆共享存放对象实例OOM方法区/元空间共享类元信息、常量、静态变量OOM可配置线程私有部分程序计数器指向当前线程正在执行的字节码行号是唯一不会 OOM 的区域。虚拟机栈里每个方法调用对应一个栈帧如果线程请求的栈深度超过虚拟机允许的深度会抛 StackOverflowError如果栈允许动态扩展但申请不到内存会抛 OutOfMemoryError。本地方法栈服务于 native 方法逻辑类似。线程共享部分堆存放对象实例几乎所有对象都在这里分配OOM 场景最常见。方法区在 JDK8 之后改成元空间使用本地内存存放类元信息、常量、静态变量等。元空间默认使用系统内存大小但也可以通过-XX:MaxMetaspaceSize限制。这里有一个细节值得提JDK7 的永久代在 JDK8 被元空间替代字符串常量池和静态变量从永久代移到了堆中。如果面试官问“为什么用元空间替代永久代”可以答“永久代大小难以设置实际开发中经常遇到 java.lang.OutOfMemoryError: PermGen space而元空间使用本地内存大大降低了这类 OOM 的概率”。面试官常追问对象分配过程新对象一般先在 Eden 区分配Young GC 后存活对象进入 Survivor 区每熬过一次 GC 年龄加 1默认到 15 岁进入老年代。大对象直接进老年代避免在新生代反复拷贝。如果老年代也放不下就会触发 Full GC还是不行就 OOM。这类题正确姿势是边说边画一张内存布局图画图的过程本身就说明你脑子里有完整模型。4.2 问题八类加载与双亲委派什么时候会打破它类加载过程是加载、验证、准备、解析、初始化。这里最容易考到的是“准备阶段和初始化阶段的区别”准备阶段给 static 变量分配内存并设置默认值int 类型默认 0引用类型默认 null初始化阶段才会执行静态代码块和显式赋值操作。很多人会把这两个阶段混在一起一追问就露馅。双亲委派模型是重点。三个默认类加载器Bootstrap启动类、Extension/Platform扩展类、Application应用类。加载一个类时先逐级向上委托给父加载器父加载器能加载就用父加载器的父加载器加载不了才轮到子加载器。这样做最大的好处是防止核心类库被篡改比如 java.lang.String 只能由 Bootstrap 加载应用程序自定义的同名类不会被加载进来。面试官通常还会问“如何打破双亲委派”。最直接的办法是继承 ClassLoader 并重写 loadClass 方法把委派逻辑改掉。实际框架中Tomcat 的 WebAppClassLoader 就是先尝试自己加载 Web 应用下的类再委派给父加载器这样每个 Web 应用可以拥有独立的类版本。SPI 机制也绕过了双亲委派比如 JDBC 的 DriverManager 是 Bootstrap 加载的但具体驱动实现却在 classpath 下需要上下文类加载器来加载。能答到这层说明你对类加载的理解已经超过大多数候选人。4.3 问题九线程池七个参数以及任务提交后发生了什么线程池这题在 Java 基础面试里出现频率极高因为几乎所有后端项目都用过线程池但能把执行流程说完整的人不多。先说七个参数corePoolSize 核心线程数、maximumPoolSize 最大线程数、keepAliveTime 非核心线程存活时间、unit 时间单位
返回列表