面试官抛出“String是引用类型吗”这种问题你以为能敷衍过去对方却在下一秒追问“那String str new String(abc)和String str abc创建了几个对象”——很多人当场翻车。这类看似基础的Java题恰恰是面试官最喜欢的深挖点因为它们能瞬间测出你是背答案还是真理解底层。常量池陷阱String的不可变性只是个幌子面试官从来不问String能不能变他们问的是“intern()方法到底干了什么”。默认情况下String s1 abc会先去字符串常量池里找找不到才创建。而String s2 new String(abc)直接堆里新建不管常量池有没有。更恶心的变体是“String s3 s1 s2”和“String s4 a b c”的区别前者运行时拼接调用StringBuilder后者编译期直接折叠成常量“abc”。关键点在于常量池里只保存字面量new出来的对象哪怕内容相同也是独立堆内存。而intern()方法能做到“手动入池”——如果池中有同值字符串返回池中引用否则在池中创建并返回引用。这个机制直接关联到JVM的元空间或永久代不仅考语法还考内存分区。面试官常会继续追问“String、StringBuilder、StringBuffer的适用场景”。很多人答“StringBuffer线程安全”但紧接着就被问“为什么StringBuffer安全而StringBuilder不安全具体哪一行代码不安全”——最经典的答案是StringBuffer的每个append方法都加了synchronized但如果你连续调用两次append中间没有锁线程依然可能被插队。所谓的“线程安全”仅指单个方法的原子性而非复合操作的原子性。Integer缓存那个-128到127的坑Integer的面试题是“经典送命”题Integer a 127; Integer b 127; a b 是 true 还是 false改成128就变false。关键在于Integer类内部维护了一个IntegerCache默认缓存-128到127的Integer对象。自动装箱Integer.valueOf()会返回缓存对象所以比较引用时相同超过范围则new新对象引用不同。面试官会接着问“那你觉得Integer a 1; Integer b 1; a b 的结果跟编译器版本有关系吗”其实跟版本关系不大跟缓存上限有关。可以通过JVM参数-XX:AutoBoxCacheMaxsize增大上限但建议不要乱改。更深一层面试官可能考“Long、Short、Character有没有缓存”——LongCache范围也是-128~127CharacterCache是0~127Double和Float没有缓存因为不是一个整数范围概念。还有一个变体Integer.valueOf(128) Integer.valueOf(128)是false但int a 128; Integer b 128; a b答案是true因为自动拆箱后int比较值。这些细节翻来覆去考的就是你对自动装箱/拆箱和对象缓存的理解深度。集合类HashMap的扩容死链只是入门“HashMap是线程安全的吗”——当然不。但面试官要的不是“不安全”而是“为什么并发下会形成死循环”。JDK 1.7的HashMap在rehash时采用头插法多线程同时put触发扩容可能导致环形链表get时死循环。JDK 1.8改用尾插法解决死循环但依然有数据覆盖问题多线程put时可能互相覆盖导致数据丢失。更深层的问题是“HashMap扩容阈值为什么是0.75不是0.5或1.0”这涉及到泊松分布与哈希冲突的概率权衡0.75时桶中链表长度超过8的概率极低约千万分之一空间与时间达到平衡。0.5太浪费空间1.0又会导致高冲突。面试官还喜欢问“ConcurrentHashMap怎么实现线程安全”JDK 1.7用Segment分段锁每段一个ReentrantLockJDK 1.8改用CAS synchronized锁住链表或红黑树的头节点。注意JDK 1.8的ConcurrentHashMap的size()方法不再需要加锁而是通过CounterCell和LongAdder累加器实现高并发计数。没有理解这个演化过程就会觉得“synchronized比ReentrantLock更简单”是倒退实际上分段锁在大量线程竞争下反而会导致竞争概率上升。ArrayList vs LinkedList你背的结论可能是错的很多人被教育“ArrayList适合随机访问LinkedList适合插入删除”。面试官会反问“那在头部插入100万条数据谁更快”答案是ArrayList更慢不对实际测试中ArrayList的批量插入比如使用addAll可能比LinkedList快因为LinkedList每个节点都要创建Entry对象且频繁修改指针而ArrayList只是随机移动一次内存。真正决定性能的是插入位置和元素个数而非数据类型。还有一个细节ArrayList的ensureCapacity方法。如果你预先知道要插入大量数据先调用ensureCapacity可以避免多次扩容复制。面试官可能让你手写一个“动态扩容数组”看你能不能写出正确的grow策略——通常是1.5倍且要处理溢出情况。异常体系checked exception是糟粕还是精华“RuntimeException和Exception有什么区别”很多人回答“运行时异常不需要try-catch”。面试官会追问“那你觉得写代码时应该尽量用检查异常、自定义异常还是只在运行时抛”经典误区是认为检查异常增加了代码健壮性但大量实践表明过度使用checked exception会让业务代码被try-catch淹没降低可读性。比如Spring、Hibernate等框架普遍倾向于抛非检查异常如DataAccessException、BeanCreationException因为调用方同样无法理性处理每一个SQL异常。面试官还会深问“try-with-resources的底层原理”。你如果只知道自动关闭是因为实现了AutoCloseable那不够。真正原理是编译器会将try-with-resources编译成字节码用try-finally嵌套并且捕获的异常被标记为“被抑制的异常suppressed”。了解suppressed异常对于调试多资源关闭场景非常重要比如两个资源都抛出异常如果不处理suppressed异常只会得到最后一个异常前一个异常被丢弃。线程基础wait和sleep的区别不止一个“Object.wait()和Thread.sleep()有什么不同”最浅的回答是“wait释放锁sleep不释放”。但面试官会深究“notify之后wait线程马上能获得锁吗”——notify只是把线程从等待集移到锁竞争队列真正获得锁还需要等到持有锁的线程退出同步块。这也是为什么在synchronized块里notify之后当前线程仍可能继续执行后续代码因为锁还没有释放。另一个经典问题是“为什么wait、notify必须在synchronized块里调用”因为wait需要先检查条件比如队列满而条件的变化可能被另一个线程同时修改如果不加锁会出现“虚假唤醒”spurious wakeup。Java官方文档中明确要求wait在循环中调用while(condition) wait()而不是if就是因为虚假唤醒的存在。JVM内存模型堆栈溢出问题不是靠调参数解决“栈溢出和堆溢出怎么定位”面试官喜欢问具体的工具链。栈溢出通常是因为递归调用过深或线程栈空间太小可以通过-Xss调整。但更深入的问题是“一个Java线程的栈默认多大”通常是1MB但在32位系统只有256KB。面试官会让你估算“一个递归调用大概消耗多少栈帧”每个方法调用会压入局部变量表、操作数栈、动态链接、方法出口等大概几百字节到几K字节不等。堆溢出通常用分析heap dumpjmap jhat或MAT。但很多人不知道如何触发堆溢出——用-Xmx设置很小的堆然后不断创建大对象并保持引用。面试官会追问“如果堆内存被占满但GC一直没有触发是什么情况”可能是因为对象都属于不可回收的如static集合GC无法回收导致频繁Full GC反而性能下降。真正的深度在于理解GC算法和内存泄漏的常见模式比如ThreadLocal使用不当导致内存泄漏、ClassLoader未卸载导致元空间泄漏等。反射和代理动态代理和CGLIB的底层差异“JDK动态代理为什么只能代理接口”因为JDK动态代理在运行时生成一个继承自Proxy的类该类实现了你指定的接口但如果目标类没有接口就是普通类就无法实现。而CGLIB直接通过ASM字节码生成目标类的子类所以能代理普通类。区别在于CGLIB对final方法无法代理因为不能覆盖而JDK动态代理只能用接口。面试官会继续问“Spring如何选择使用哪种代理”如果类实现了至少一个接口默认使用JDK动态代理否则用CGLIB。但可以强制启用CGLIB。还要注意CGLIB生成的代理类会多出一个MethodProxy对象比JDK动态代理的性能稍高但创建过程较慢。另一个深挖点是“反射调用的性能为什么差”因为反射需要检查可见性、参数类型并且每次调用都要进行方法查找。JVM对反射有一定的内联优化如sun.reflect.MethodAccessor接口但超过一定次数后会自动生成字节码来提升性能阈值默认是15次。这些细节如果不清楚就无法解释为什么在极热路径上应避免使用反射。并发工具类CountDownLatch和CyclicBarrier哪个是“一次性”“CountDownLatch和CyclicBarrier有什么区别”标准答案是CountDownLatch是一个线程等待多个线程完成只能使用一次CyclicBarrier是多个线程互相等待可以用reset()重置。面试官追问的重点是CyclicBarrier的reset()会导致正在等待的线程抛出BrokenBarrierException如果你没处理这个异常程序可能挂掉。再有就是CountDownLatch的await()可以带超时CyclicBarrier也可以带超时但会破坏屏障。另一个常考的是Semaphore“acquire()和release()一定要成对使用吗”——如果release()调用次数多于acquire()会导致许可数量增加下个线程不需要获取许可也能通过。这种许可泄漏在生产环境中是一个非常隐蔽的bug。序列化transient和static修饰的字段会怎样“实现Serializable接口的类如果某个字段用transient修饰序列化时会怎样”理所当然是不被序列化。但面试官会接着问“那如何自定义序列化”你可以实现writeObject和readObject方法或者扩展Externalizable接口。更深的问题是序列化版本号——serialVersionUID。如果没有显式声明编译器会自动生成但类结构一旦变化比如新增字段生成的值会变导致序列化兼容性问题。所以最佳实践是显式声明一个固定的UID。一个更冷门的知识枚举类型的序列化。Java枚举默认实现了Serializable但反序列化时不会创建新的对象而是使用枚举常量本身通过valueOf方法查找。这保证了枚举的单例性即使有多个实例也是同一个引用。而普通的单例类如果不做特殊处理比如readResolve方法反序列化时会打破单例创建新对象。结尾面试官为什么就爱深究因为基础题最能体现一个人对语言的敬畏程度。比如“equals和hashCode的约定”很多人知道要重写两者但没考虑过“如果一个对象参与HashMap且之后该对象的某些字段变了会导致哈希码改变而HashMap使用原哈希码存放用新哈希码查找时找不到对象”。这就是内存泄漏的典型场景——把一个可变对象作为HashMap的key然后在外部修改它的字段。面试官通过设问这些“小细节”实际上是在考察你的工程直觉是否养成。如果你能把上述每个点都讲清楚、举出实例甚至能说出JDK不同版本的细微差异那面试官就不会再在基础环节追问下去。相反若你停留在“String是不可变的”或“HashMap是数组链表”的层面下一个问题就是“那红黑树什么时候开始退化”——大多数人又卡住了。所以真正的深度不在多复杂的框架而在于你对自己写下的每一行代码都抱有底层好奇。