面试官把简历翻到第三页指尖在“熟练掌握Java”那行字上停了一秒。我坐在对面看着他把咖啡杯推到一边知道接下来的十五分钟又要从那些最基础的问题开始。十年了从实习生面到高级工程师这十道题像旧识一样反复敲门。它们太轻轻到被无数博客和刷题视频敷衍带过它们又太重重到每一次回答的深度都能直接照出一个人对Java这门语言的理解究竟停留在语法表面还是已经扎进了虚拟机与内存模型的土壤里。第一道equals和hashCode的契约你真的遵守了吗许多候选人能背出“重写equals必须重写hashCode”但当我追问“为什么两个对象相等时hashCode必须相等”时会议室里常常陷入沉默。这个契约的本质不是规则而是哈希表的生存逻辑。HashMap在put时先用hashCode定位桶再用equals确认键是否相同。如果两个equal的对象散列到不同的桶那么用其中一个去get另一个时你会得到null——数据明明存在却永远找不到。更隐蔽的坑藏在集合的“可变键”里。我见过有人把对象放进HashMap后修改了参与hashCode计算的字段从此那个键就永远锁死在旧桶里再也无法获取。hashCode是对象在哈希世界的门牌号equals是门牌号对应的真实身份两者不匹配整个集合体系就会陷入逻辑混乱。面试官真正想听的不是背契约条文而是你是否理解HashSet去重、HashMap检索、以及缓存场景下因重写不当而引发的内存泄漏风险。第二道String为什么设计成不可变这个问题看似送分却能筛选出思考深度的分水岭。浅层回答是“因为不可变所以线程安全、可以缓存hashCode、常量池复用”。但深层追问来了如果String可变StringBuilder为什么还要存在答案在于拼接效率——不可变性导致每次拼接都要新建对象所以JDK才提供可变字符序列。还有一道潜台词不可变类如何保证不被反射破坏实际上反射可以修改私有final字段因此真正的不可变还需要设计者不暴露修改入口并且把字段声明为final。String的不可变不是语言强制的铁律而是一场精心设计的集体共识每个Java程序员都是这场共识的受益者与守护者。面试官会用这个题测试你对“机制”与“目的”的区分能力。当你提到“常量池缓存字符串字面量”时不妨补一句正因为不可变哈希码才能安全地缓存String才能成为HashMap最常用的键类型。第三道ArrayList和LinkedList谁是谁的救赎网上流行答案总是“ArrayList查快增删慢LinkedList增删快查慢”但一到真实场景就露馅。让我把压力给到增删LinkedList的“增删快”只体现在头部或指定位置且已经持有了该位置节点的场景。如果你直接调用add(int index, E element)它内部依然要遍历到index附近才能插入时间复杂度是O(n)和ArrayList的位移成本半斤八两。更致命的是LinkedList每个节点需要额外存储两个引用内存占用比ArrayList多出一大截。把LinkedList当“万能增删神器”是一种被教科书美化的误读现代Java开发中它几乎活在阴影里。那么ArrayList的扩容机制呢默认容量10每次扩容为原来的1.5倍这是通过位运算oldCapacity (oldCapacity 1)实现的。面试官期待你答出缩容策略、modCount的fail-fast机制以及为什么foreach遍历时不能增删元素——因为迭代器会检查modCount修改了集合就会抛ConcurrentModificationException。第四道HashMap的桶里到底装了多少秘密这道题几乎霸占所有Java面试的C位。从JDK7的数组链表到JDK8的数组链表红黑树演进逻辑指向一个核心哈希冲突的代价必须被控制在可接受的范围内。当链表长度超过8且数组容量超过64链表转为红黑树把查找复杂度从O(n)降到O(log n)。容量为什么是2的幂次因为hash (capacity - 1)等价于取模且位运算比取模快同时保证扩容后元素索引要么原地不动要么移动旧容量大小的偏移量。还有一个致命细节为什么初始容量是16这个数字是经验平衡——太小则频繁扩容太大则浪费内存。面试官最爱追问“死循环到底怎么回事”——这是JDK7在并发扩容时头插法导致的环形链表问题JDK8改为尾插法后避免了此问题但HashMap始终不是线程安全的容器。你可以在并发下用ConcurrentHashMap但永远不要指望HashMap在竞态中给你惊喜。第五道synchronized到底锁住了什么从一个最容易被轻视的细节说起synchronized修饰实例方法锁的是this修饰静态方法锁的是Class对象修饰代码块锁的是括号里的对象。很多人把这三者混为一谈结果写出了看似同步实则并发的bug。举一个真实的例子一个类有两个静态同步方法和两个实例同步方法四个线程同时访问四个方法你猜会有几种进入方式答案是两种——静态锁和实例锁互不干涉。锁的粒度决定了并发度也决定了安全性你选择哪把锁就是选择哪一种权衡。JVM对synchronized做了大量优化偏向锁、轻量级锁、重量级锁的升级是不可逆路径。偏向锁会记录线程ID当竞争出现时撤销偏向升级为轻量级锁自旋失败后膨胀为重量级锁依赖操作系统的互斥量。现代JDK的synchronized性能已经大幅提升但面试官更愿意看到你区分它和ReentrantLock的差异可中断、超时、公平锁、多个条件队列。第六道volatile的可见性敌不过原子性每当候选人说“volatile能保证线程安全”我就想让他写一段单例双重检查锁的代码。DCL里instance字段必须加volatile原因不是可见性——而是禁止指令重排序。instance new Singleton()在字节码层面是三步分配内存、初始化对象、将引用指向内存。如果不加volatileJIT可能把后两步重排导致另一个线程拿到一个尚未初始化的半成品对象。volatile解决的是“一个线程写、多个线程读”的可见性问题它从不承诺复合操作的原子性。比如count这个操作volatile修饰的count依然不是线程安全的因为读取、加一、写回是三条独立指令。深入一点的面试官会继续问内存屏障volatile写之前插入StoreStore屏障之后插入StoreLoad屏障volatile读之后插入LoadLoad和LoadStore屏障。这些屏障像边境检查站阻止指令越过国境线从而建立happens-before关系。第七道线程池的核心参数是面试官最爱的剧本杀ThreadPoolExecutor有七个参数但多数人只背得出corePoolSize和maximumPoolSize。真正的考点在于执行流程与拒绝策略。当提交一个任务时线程数小于核心数则创建新线程达到核心数则放入队列队列满了且线程数小于最大数则创建临时线程临时线程也满了才触发拒绝策略。我常问的陷阱是如果corePoolSize设为10maximumPoolSize设为20队列容量为100那么第130个任务到来时会发生什么答案是——前10个直接运行第11到第110个进队列第111到第130个创建临时线程运行。第131个触发拒绝策略。这个流程比想象中更依赖队列类型是SynchronousQueue直接递交还是LinkedBlockingQueue无界队列无界队列会让maxPoolSize形同虚设因为任务永远堆在队列里。拒绝策略有四种AbortPolicy抛异常、CallerRunsPolicy调用者执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃最老。线程池不仅是一池线程更是一个拥塞控制网关参数配错系统就成了一台先吞后吐的劣质机器。第八道JVM内存模型的五块领地一块都不能混淆程序计数器、虚拟机栈、本地方法栈、堆、方法区——这五块区域中只有堆和方法区是线程共享的其余都是线程私有。面试官常问“哪些区域会抛出OutOfMemoryError”这比背定义更考验对运行时结构的理解堆溢出、方法区元空间溢出、虚拟机栈连OOM都不抛而是抛出StackOverflowError因为栈深度超限。还有一道经典的“对象一定在堆上分配吗”——答案不是绝对的。JIT通过逃逸分析如果对象未逃逸出方法就可能被分配到栈上随着方法结束自动销毁连GC都不用触发。栈上分配的优雅之处在于把一次垃圾回收的负担转化为一次栈帧弹出的即时清理。另一个值得深思的点是Java 8彻底移除永久代用元空间替代把类的元数据从堆内搬到本地内存。这样一来元空间溢出不再受JVM堆大小限制而受系统可用内存约束。你需要理解方法区是逻辑概念元空间是物理实现两者关系如同接口与实现类。第九道垃圾回收的根身在哪里“怎么判断一个对象可以回收”引用计数法被淘汰是因为循环引用无法解决所以主流JVM用可达性分析。GC Roots包括虚拟机栈引用的对象、静态属性引用的对象、常量引用的对象、JNI本地方法栈引用的对象。一个对象即使被标记为不可达也不是立刻死亡它还有一次在finalize()中自我救赎的机会——当然这种机会在正经生产代码中永远不该被依赖。真正让面试官精神一振的是分代收集理论新生代对象的生死密度极高适合复制算法老年代对象存活率高适合标记-整理或标记-清除。垃圾回收器也是一部家族史Serial单线程、Parallel多线程并行、CMS并发低停顿、G1全区域化、可预测停顿、ZGC着色指针读屏障停顿低于10ms。你至少需要说清G1如何把堆分成Region以及它的Remembered Set如何避免全堆扫描。GC调优的本质不是选择最强的回收器而是理解每个内存区域的容量与对象生命周期让回收动作恰好发生在最不疼的瞬间。第十道类加载的“双亲委派”到底在防止什么Bootstrap ClassLoader、Extension ClassLoader、Application ClassLoader构建出一条层级链。加载类时先让父加载器尝试父加载器无法加载时才由子加载器自己处理。这个机制的核心价值不是性能而是安全防止核心API被篡改。比如你写了一个java.lang.Object双亲委派会让Bootstrap先加载rt.jar里的真身你写的那个垃圾类根本不会被加载。但面试官往往接着问“双亲委派一定能被打破吗”——你答“可以通过自定义ClassLoader重写loadClass方法”这还不够要说出打破的场景Tomcat为每个Web应用提供独立类加载器保证同一个类在不同应用中互不干扰JDBC通过ServiceLoader机制绕开双亲委派让驱动程序从应用类路径中加载。双亲委派是一种默认秩序而框架用不同的加载器在秩序中开凿了渠道理解这些渠道才算真正理解了Java的动态性。如果你还能补一句“类加载器和线程上下文类加载器Thread.currentThread().getContextClassLoader()的关系”这场问答基本就封顶了。走出面试间的时候我常想起自己刚入行时对这些题目的稚嫩回答。它们像十面镜子每一面都映出你对内存、并发、类结构的理解深浅。基础问题不是测试记忆的题库而是检验思维方式的试金石——面试官从不会因为你会背答案而录用你只会因为你能在答案背后看见设计的权衡与运行的真相而对你点头。当你要去下一场面试时与其刷一百道新题不如把这十道旧题重新咀嚼一遍因为在Java的世界里真正的高深恰恰藏在那些被问了十年还没过时的基础之中。