ARTICLE DETAIL

资讯详情

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

Java面试基础篇核心考点全解析:从集合到JVM一次讲透

Java面试基础篇核心考点全解析:从集合到JVM一次讲透 这个标题拿出来我自己都愣了一下。Java面试八股居然敢说这一篇就够还只限基础篇这是多大的口气。不过转念一想恰恰是基础篇这三个字才是整句话的良心所在。Java基础部分的面试题覆盖面极广从面向对象到集合、从异常到并发、从JVM到新特性随便一列就是几百个问题想用一篇博文说全那是痴人说梦。但如果说的是把面试官最常问、最容易连环追问、最能区分水平的那些题目讲透那是可以做到的也是我这次想认真做的事——把这几年面试别人和被别人面试的经验浓缩成一篇能真正拿来复习干活的文章而不是那种复制粘贴的题库。这篇内容适合谁三类人。第一类准备校招或跳槽、正处于Java复习期的同学你可以把它当提纲看完一个点去深挖一个点第二类有两年左右工作经验、想自查基础有没有漏洞的工程师我的经验是很多线上事故追根溯源都是基础概念理解不透第三类面别人的面试官可以看看我梳理的这些追问角度有没有你能直接用的。我尽量把每个点都按面试官怎么问→你该怎么答→追问怎么顶住的逻辑来讲文末再附上我实际复习时踩过的坑和时间分配建议。1. 内容整体设计与思路拆解先说个大实话八股文这个东西网上铺天盖地收藏夹里吃灰的没有一千也有八百。为什么大多数人背了还是挂因为背的方式错了把知识点当孤立的信息点去记而不是当一条逻辑链去理解。面试官一句为什么就能把背答案的人打回原形。我这篇文章的核心思路不是把八股答案喂给你而是把每个高频考点背后的为什么拆开。比如HashMap不只要背数组加链表加红黑树要能讲清楚为什么阈值是8、为什么加载因子是0.75、为什么链表转红黑树之前还要判断数组长度。这些细节不是面试官刁难人而是它们实打实决定了代码的性能和行为线上调优时会用到。另外我刻意把知识点按高频程度和追问深度分了优先级。你在有限时间内复习不可能面面俱到那就先把最高频、最容易被连环追问的点吃透冷门边缘知识点扫一遍有个印象即可。这篇文章里我把所有内容分为三个梯队第一梯队是必考点几乎每场面试都会遇到面向对象三大特性、String、集合框架特别是HashMap、异常处理、JVM内存模型。这些东西是基本面答不好直接出局。第二梯队是加分点考察你是否真的写过代码、理解原理反射、泛型、注解、并发基础、Lambda与Stream。这些往往在第一梯队答完之后追问能接住就说明你不是背题选手。第三梯队是区分点用来区分会写Java和懂Java类加载机制、SPI、动态代理、序列化、锁升级过程、ThreadLocal原理。这些在高级岗位或者一面快结束时出现答好了很加分。打个比方第一梯队是基本功像练武之人的马步第二梯队是招式像擒拿手第三梯队是内功心法决定你能不能融会贯通。大多数面试者倒在了马步上少数人招式漂亮内功到位的凤毛麟角。2. 面向对象三件套封装、继承、多态的面试陷阱2.1 面试官最爱问的多态到底考什么面向对象三大特性封装、继承、多态几乎是每场Java面试的开胃菜。但你别小看这道开胃菜我见过太多人在多态这个点上翻车。面试官经典问法什么是多态Java里多态是怎么实现的很多人张口就来多态就是同一个方法在不同对象上有不同的表现形式。这只能给60分因为你只说到了表现层没有说到实现层。Java多态的实现依赖三个条件继承或实现接口、方法重写、父类引用指向子类对象。运行时JVM会根据对象的实际类型来决定调用哪个方法这就是动态绑定。这里面试官的追问链条通常是静态方法和私有方法能被重写吗答不能。静态方法属于类是静态绑定私有方法不可见也不存在重写。那父类引用调用子类特有的方法行不行答不行编译期就要报错因为编译看的是引用类型。属性成员变量有重写这一说吗答没有。属性是静态绑定的看引用类型。我建议你把这个追问链亲手用代码验证一遍写个小demo输出结果会颠覆很多人的直觉——比如Father father new Son(); father.name拿到的还是父类的name。这种代码跑一遍比背十遍理论都管用。2.2 构造方法、初始化顺序与继承的暗坑关于继承面试里还有一个高频陷阱题子类创建时父类和子类的初始化顺序是什么这个题目几乎每次面试都会被问到而且变化多端。标准答案父类静态代码块 → 子类静态代码块 → 父类非静态代码块 → 父类构造方法 → 子类非静态代码块 → 子类构造方法。但面试官不会满足于这个顺序他会追问如果父类构造方法里调用了被子类重写的方法会发生什么这个问题就刁钻了。实际结果是会调用子类的重写方法而且此时子类的成员变量还没初始化完毕你拿到的可能是默认值null、0。为什么因为初始化顺序是先父后子但动态绑定是看实际类型。这就是为什么业界强烈建议不要在构造方法里调用可重写的方法。这不是理论洁癖这是实实在在的坑Spring的依赖注入在某些场景下也会踩到类似的问题。我自己的习惯是面试中碰到这类题先给结论再讲原理最后补一句所以实际开发中我尽量避免在构造器里做太复杂的事。这句话一出来面试官对你的印象分立刻不一样因为这说明你不是背答案而是真的被坑过。2.3 抽象类与接口怎么选抽象类和接口的区别也是基础篇的常客。传统答案会说抽象类可以有构造方法、可以有成员变量、可以有具体方法接口在Java 8之后可以有default方法和static方法Java 9之后还允许私有方法。单继承与多实现的区别一个类只能继承一个抽象类但可以实现多个接口。但更高级的答法是谈语义差异抽象类表达的是是什么is-a强调代码复用接口表达的是能做什么has-a / can-do强调能力契约。在架构设计里优先用接口定义行为规范用抽象类做模板方法复用公共逻辑。面试官如果追问JDK里有没有哪个设计同时用了抽象类和接口你可以举java.util.AbstractList和java.util.List的例子List定义规范AbstractList提供公共实现骨架ArrayList、LinkedList各取所需。这就是模板方法模式的典型应用还能顺带带出设计模式一箭双雕。3. 数据类型与String连环追问的重灾区3.1 基本类型、包装类型与缓存池Java的数据类型这块面试官特别喜欢从一些看似简单的问题开始挖。比如int和Integer有什么区别如果你只答int是基本类型Integer是包装类型那就等着被连环追问吧。完整的回答应该包含默认值int是0Integer是null、存储位置基本类型局部变量在栈上包装类型是对象在堆上、使用场景泛型只能用包装类型、集合只能放包装类型、以及自动装箱拆箱的底层实现valueOf和intValue方法。再往下挖就是经典考点Interger缓存池。Integer默认缓存-128到127之间的对象超出这个范围new Integer(128)和Integer.valueOf(128)得到的是不同对象。但用比较时Integer a 128; Integer b 128; 比较结果是false因为此时走的是新建对象。这个坑我在实际代码review里见过不止一次尤其在处理数据统计、主键比较的时候一眼没看住就出事。我的建议是不是基本类型和包装类型之间的比较一律用equals别用。这条经验写进团队规范能少踩一半的坑。3.2 String、StringBuilder、StringBuffer三兄弟String相关的问题是Java面试的基础重头戏也是区分背题党和真懂的人的利器。先背一层String是不可变的StringBuilder线程不安全但效率高StringBuffer线程安全但效率低。这个答案能过关但拿不到高分。追问马上就来String为什么设计成不可变的这个问题能考出你对JVM和语言设计的理解。关键原因有三个字符串常量池的复用。只有不可变才能安全地让多个变量引用同一个字符串对象否则一个变量的修改会影响到所有引用那池子就失去意义了。安全性与线程安全。String被广泛用作参数、类名、网络地址等如果可变很容易被恶意篡改不可变天然线程安全。哈希码缓存。String的hashCode被大量使用在HashMap等场景中不可变才能放心缓存不用每次重新计算。再追问一层String拼接用和concat有什么区别在循环里用会不会有问题这个坑很实际。String是final类用拼接时JVM会优化成StringBuilder操作但在循环里拼接每次循环都会new一个新的StringBuilder造成不必要的对象创建性能比显式用StringBuilder差一个数量级。我在做性能调优时见过一个日志字符串拼接的循环几百毫秒的操作被拖到几秒改成StringBuilder之后立竿见影。3.3 equals与hashCode的约定equals和hashCode这个问题我认为是Java基础面试中最能体现程序员功底的问题之一。不仅概念要清楚还得能聊出为什么需要这样设计。基本定义equals用于判断两个对象内容是否相等hashCode返回对象的哈希码。约定是两个对象equals返回truehashCode必须相等两个对象hashCode相等equals不一定为true这就是哈希冲突。面试官为什么要问这个因为实际开发中我们经常自定义对象作为HashMap的key如果不重写这两个方法HashMap就会用Object的默认实现比较引用地址你new两个相同内容的对象去get就是get不到线上bug就这么来的。更深一点面试官会问为什么先比较hashCode再比较equals答案是性能。HashMap查找时先用hashCode定位到桶再在桶里用equals确认。如果只用equals每个桶里的元素都得挨个比较O(n)复杂度用hashCode定位到桶是O(1)代价就是必须保证hashCode相同才可能equals为true。还有一个高频追问HashMap用可变对象做key会出现什么问题这个问题我能讲的坑太多了。比如用一个Person对象做key存进Map然后改了它的年龄字段hashCode变了再去get就找不到了因为存储时的桶位置是根据旧hashCode算的。解决方案是作为key的对象要么不可变要么一定不要修改会影响hashCode的字段。这条经验在真实业务中经常踩到我见过有人因为在缓存Map的key里改了对象字段导致缓存全部失效排查了半天。4. 集合框架源码级考点批量拆解4.1 ArrayList扩容机制与fail-fast集合框架是Java面试中最大的一块硬骨头高频考点几乎都在这里。先说ArrayList面试官最爱问扩容机制。ArrayList底层是Object数组初始容量是10懒加载第一次add时才创建每次扩容到原来的1.5倍。JDK 8的实现用的是右移运算int newCapacity oldCapacity (oldCapacity 1)。你可能会问为什么是1.5倍因为扩容涉及数组拷贝代价很高太频繁扩容性能差但扩太大又浪费内存。1.5倍是一个经验平衡值。fail-fast是另一个高频考点。ArrayList在迭代过程中如果结构被修改add、remove会抛出ConcurrentModificationException。底层原理是modCount计数器迭代器初始化时记录expectedModCount modCount每次next()时检查两者是否一致不一致就抛异常。这个机制不是为了线程安全而是为了快速暴露并发修改的bug。追问版本为什么Java 8中List.removeIf不会抛ConcurrentModificationException因为removeIf内部用了Itr的remove方法每次删除后都会同步expectedModCount所以不会触发检验。这个细节很偏但答出来绝对惊艳。4.2 HashMap的底层结构与put流程HashMap是集合框架里最大的一座山几乎每个面试官都会从这里出招而且会问得很深从数据结构问到红黑树再问到扩容resize。你必须把整个流程串成一条线来理解。先背基础JDK 8之后HashMap底层是数组链表红黑树。初始容量16加载因子0.75当数组元素个数超过容量×加载因子时触发扩容容量翻倍。put流程是先对key的hashCode做扰动运算高16位与低16位异或然后按hash与数组长度减一取模定位到桶如果桶为空直接放不为空就遍历链表key相同则覆盖否则尾插法添加。链表长度超过8且数组长度超过64时链表转为红黑树。面试官必问的一个点是为什么加载因子是0.75这个问题有标准解释这是空间和时间的一个权衡。加载因子太高比如1.0可以减少扩容次数节省空间但哈希冲突概率增大链表变长查询效率下降加载因子太低比如0.5冲突少了查询快了但每次扩容都要多拷贝数据浪费空间。0.75是泊松分布推导出来的一个比较合理的选择在这个值下HashMap桶中元素个数超过8个的概率约为千万分之六所以链表转红黑树的阈值选8。再问一个高难度的HashMap在并发下会有什么问题JDK 7时代并发put可能造成链表环导致get死循环CPU飙到100%JDK 8修了这个bug但并发put仍然有数据覆盖的问题。所以高并发场景必须用ConcurrentHashMap不是HashMap加个锁就完事。4.3 ConcurrentHashMap如何保证线程安全ConcurrentHashMap在Java 8之后的实现也是面试必考的内容。JDK 7的ConcurrentHashMap采用分段锁将数据分成多个Segment每个Segment持有一把锁不同的线程操作不同的Segment时互不干扰从而提升并发度。JDK 8放弃了分段锁改用CASsynchronized只锁链表的头节点锁粒度更细并发度更高。具体来说当线程put时如果桶为空用CAS直接插入不加锁如果桶不为空对头节点加synchronized锁再在链表或红黑树中插入。所以这里把锁的粒度缩小到了单个桶相比JDK 7的Segment锁又前进了一步。面试官还可能追问为什么JDK 8用synchronized而不用ReentrantLock因为synchronized在JDK 6之后引入了偏向锁、轻量级锁的升级过程性能已经得到大幅提升而且synchronized是JVM内置的不需要手动释放锁代码更简洁。加上ConcurrentHashMap的锁持有时间极短synchronized完全足够还能减少代码复杂度。4.4 TreeMap与LinkedHashMap怎么选除了ArrayList和HashMap面试偶尔还会问TreeMap和LinkedHashMap。TreeMap底层是红黑树元素按键的自然顺序或Comparator排序存储查找、插入、删除都是O(log n)。面试官爱问什么场景用TreeMap——需要有序遍历时比如按时间排序的报表、区间查询等。但如果只是插入后排序一次我更建议用ArrayList存下来再Collections.sort因为TreeMap的插入成本比ArrayList高不少没必要为了一辈子的排序付出每次插入的代价。LinkedHashMap底层是HashMap加双向链表维护插入顺序或访问顺序。访问顺序场景很经典实现LRU缓存。继承LinkedHashMap重写removeEldestEntry方法当缓存大小超过指定容量时返回true自动删除最久未访问的节点。这个实现我在写本地缓存时经常用几行代码搞定比引入一个缓存框架轻量太多。5. 异常处理try-catch-finally的底层逻辑5.1 异常体系结构与运行时异常的区别Java异常体系看起来简单但面试官会问出很多让人后背发凉的问题。先搭骨架Throwable是所有异常的根下面分Error和Exception。Error表示JVM层面的严重错误如OutOfMemoryError、StackOverflowError程序一般无法恢复不用catch。Exception分受检异常checked exception和运行时异常runtime exception。受检异常是编译器强制要求处理的继承Exception但不继承RuntimeException比如IOException、SQLException要么throws要么try-catch。运行时异常继承RuntimeException比如NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException编译期不强制处理。面试官会追问什么时候用受检异常什么时候用运行时异常这是有实际设计考量的。我的经验是对于调用方可以恢复的异常用受检异常强制处理对于调用方无法恢复的编程错误用运行时异常让它快速失败向上抛出。比如参数校验失败我会直接抛IllegalArgumentException不搞受检异常——因为你让调用方catch它也做不了什么不如早炸早发现。在Spring框架里事务默认只对RuntimeException回滚受检异常默认不会触发回滚。这一点如果你在业务代码里自定义了受检异常然后发现事务没回滚恐怕就要排查半天了。我建议自定义异常一律继承RuntimeException省心。5.2 finally里的return陷阱与try-with-resourcestry-catch-finally里最经典的坑就是return语句与finally的交互。如果try里写了returnfinally里也写了return最终返回的是finally里的值。这因为return其实分两步先把返回值保存起来再执行finally如果finally有return它会把之前的保存值覆盖掉。实际开发中我强烈建议不要在finally里写return这几乎总是代码异味容易掩盖异常排查问题时极度痛苦。另一个坑是catch里的return配合finally修改返回对象。如果try里return一个对象引用finally里修改了该对象的属性最终返回的还是那个被修改后的对象。所以对象引用不能避免finally的修改只有重新赋值一个新引用才有效。理解这一点很多面试题都能一眼看穿。Java 7之后资源释放有了更好的选择try-with-resources。对于实现了AutoCloseable的类如InputStream、Connection可以直接写try(InputStream in ...) { ... }JVM会自动关闭资源比finally里手动close更安全也更简洁。唯一的坑是在try-with-resources的try块中如果正常处理和资源关闭都抛出异常正常处理的异常会被压制suppressed你可以通过getSuppressed()拿到被压制的异常。6. JVM内存与OOM排查实战6.1 运行时数据区域与对象创建流程基础面试到了这一块就开始往JVM深水区蔓延了。面试官会问Java运行时数据区分为哪几块标准答案是程序计数器、虚拟机栈、本地方法栈、堆、方法区JDK 8之后改为元空间。程序计数器存储当前线程执行的字节码行号是线程私有的也是唯一不会OOM的区域。虚拟机栈存储栈帧每个方法调用对应一个栈帧的压栈弹栈栈帧里包含局部变量表、操作数栈、动态链接、方法返回地址StackOverflowError就是栈深度不够。本地方法栈服务native方法。堆存对象实例是GC的主战场所有线程共享。方法区存类信息、常量、静态变量JDK 8之后用元空间替代永久代元空间使用本地内存不再受JVM堆内存限制。对象创建流程也经常被考类加载检查 → 分配内存 → 内存空间初始化 → 设置对象头 → 执行构造方法。分配内存分两种方式指针碰撞和空闲列表取决于堆是否规整而堆是否规整又取决于垃圾收集器是标记-整理还是标记-清除。这套叙述在面试官看来非常加分因为它把对象创建、内存分配、GC策略串成了一张网。6.2 内存溢出与内存泄漏从热搜词的OutOfMemoryError说起标题下面挂了一堆热搜词里面包含了java: outofmemoryerror: insufficient memory这个报错这在实际开发里太常见了我单独拎出来讲。OutOfMemoryErrorOOM是JVM抛出的内存不足错误细分下来有几种常见类型Java heap space堆内存不足、GC overhead limit exceededGC一直回收但内存持续不够JVM已经快撑不住了、Metaspace元空间不足、Unable to create new native thread创建线程失败通常是系统线程数达上限。我当年在线上踩过最疼的一个坑就是GC overhead limit exceeded查了半天才发现是导数据接口把一批超大集合全load进内存了一台4G内存的机器直接被压垮。处理思路是先dump一份堆文件用MAT或jvisualvm分析哪些对象占了大量空间然后顺着引用链找代码里的问题。简单的命令jmap -dump:formatb,fileheap.hprof 然后MAT的Leak Suspects Report能直接给出嫌疑对象和引用链。这里有个细节线上机器一般内存不小dump文件巨大直接拉下来分析不现实我会先看GC日志用jstat -gcutil观察各代的使用曲线初步判断是年轻代还是老年代的问题再决定下一步。还有一种OOM但堆不高的情况很反直觉。比如java: outofmemoryerror: insufficient memory出现在JVM启动或编译阶段可能是操作系统给JVM分配内存失败而不是Java堆满了。常见原因docker容器里共享内存不够/dev/shm容量太小、swap不足、系统内存耗尽。这个排查方向特别容易忽略因为一看到OOM就习惯性去看堆方向错了特别浪费时间。注意本地开发跑不出来复现不了可以先调小-Xmx在demo上复现然后dump分析生产环境JVM建议加上-XX:HeapDumpOnOutOfMemoryError参数让OOM发生时自动落盘dump文件省得事后用jmap抓。7. 泛型、反射、注解进阶加分的动态机制7.1 泛型的本质与类型擦除泛型这个考点面试官特别爱从Java的泛型和C模板的区别切入。核心结论是Java泛型在编译期做类型检查运行时通过类型擦除把类型参数抹掉所以Java的泛型是伪泛型。为什么用类型擦除为了兼容旧代码。JDK 5引入泛型时Java已有几十万个类库和框架如果运行时保留泛型类型那所有古老的容器类代码比如裸List都无法和新代码互操作。擦除之后编译好的字节码全是裸类型老代码可以无缝运行。类型擦除引出一堆经典问题比如List 和List 在运行时是同一个类都是List你不能用instanceof检查泛型类型因为类型信息不存在你不能new T()因为编译器不知道该创建什么类型。再看一个常见应用场景TypeReference。用泛型从JSON字符串反序列化成对象如果不传TypeReferenceJackson和Fastjson都不知道该还原成什么类型只能返回LinkedHashMap。这是因为运行时泛型类型被擦除了但通过匿名子类的getGenericSuperclass()技巧可以在运行时捕获到父类泛型参数的完整类型。用大白话说Java不允许你直接访问泛型类型但如果你创建一个实际上带着泛型参数的子类JVM会在类签名里记录这个信息反射就能读出来。7.2 反射的常用API与性能权衡反射是Java框架的基石也是面试里的加分点。面试官会问反射能做什么能拿到类的所有属性和方法、能绕过private访问限制、能动态创建对象和调用方法。Common用法Class.forName、getDeclaredMethod、setAccessible(true)、invoke。再问反射有什么缺点性能比直接调用慢且存在安全隐患。为什么慢因为反射调用时要走native方法、要做权限检查、要处理参数装箱等等一套流程下来开销比正常方法调用大得多。Spring框架大量用了反射和动态代理所以Spring容器的启动比普通代码慢不少但运行时大多数是正常调用反射开销并不是主要瓶颈。如果面试官问到反射和字节码增强有什么区别你正好可以接住反射是在运行时读取和操作类信息而字节码增强ASM、CGLIB、Byte Buddy是在类加载时修改字节码直接生成新的类性能远好于反射。Spring为什么用CGLIB而不是全用JDK反射生成代理因为CGLIB操作字节码、生成子类走的是正常方法调用链性能好JDK动态代理则要求在接口上操作调用时经过InvocationHandler转发多了一层。很多框架源码读不懂卡就卡在反射和动态代理这个基础概念上。你要是能把Spring的Configuration怎么通过CGLIB代理实现单例、Transactional怎么通过代理织入事务逻辑讲清楚面试官对你的评价绝对上一个台阶。7.3 注解驱动开发与自定义注解注解在Java开发中无处不在面试也越来越多地考到自定义注解。基础是注解定义用interface元注解有Retention生命周期、Target作用目标、Documented是否进入javadoc、Inherited是否可继承。重点在Retention它有三个值SOURCE源码期编译就被丢弃比如SuppressWarnings、CLASS编译期保留运行时不可见比如Lombok的注解、RUNTIME运行时保留可以被反射读取比如Spring的Autowired。大部分框架注解都是RUNTIME因为它要在运行时通过反射解析。如果面试官问自定义注解怎么用我会回答三步定义注解 → 通过反射解析注解 → 根据配置执行对应逻辑。我曾经写过一个接口幂等性校验的注解核心是用拦截器拦截被注解标记的方法读取注解里的参数比如Redis key前缀和过期时间在方法执行前检查Redis是否已存在相同请求ID存在就拒绝重复提交不存在就写入并设置过期时间。整个过程不到一百行代码但解决了线上用户连点多次导致重复下单的问题。这比空谈注解概念要有说服力得多。8. 并发基础synchronized、volatile与JMM8.1 Java内存模型与可见性并发是Java进阶里最难啃的一块骨头但基础篇只需要覆盖核心概念。面试官必问的第一个概念是Java内存模型JMM。JMM规定所有共享变量存储在主内存中每个线程有自己独立的工作内存线程本地缓存线程对变量的操作必须先在本地内存中进行再刷新回主内存。这就带来了不一致的问题线程A改了变量线程B可能看不到这就是可见性问题。synchronized和volatile都是解决可见性的手段。synchronized通过锁机制保证同一时间只有一个线程执行临界区代码并且在释放锁时刷新本地内存到主内存volatile通过内存屏障保证每次读写都直接操作主内存。面试官一般会追问volatile能不能保证原子性答案是不能典型例子是i读改写三个步骤volatile只能保证读和写是可见的不能保证读改写的原子性。8.2 synchronized锁升级与ReentrantLock选型synchronized的底层原理在JDK 6之后发生了很大的变化现在面试基本都会问锁升级过程无锁 → 偏向锁 → 轻量级锁 → 重量级锁。无锁状态是普通对象没有竞争。偏向锁是这个锁只会被一个线程反复获取记录线程ID后续该线程再进入时直接通过没有CAS开销。一旦第二个线程来竞争偏向锁升级为轻量级锁通过自旋CAS抢夺锁适合锁持有时间短的场景。如果自旋超过一定次数或等待线程数过多升级为重量级锁由操作系统管理阻塞等待适合锁持有时间长的场景。所以锁升级是从无到有、由轻到重的。锁偏向于第一个获取它的线程所以叫偏向锁。这个设计很聪明因为大部分锁在实际情况中根本没有竞争开一个重量级锁纯属浪费资源。再说ReentrantLock和synchronized的选择。面试官如果问两个有什么区别你从这几个维度回答synchronized是JVM层面的关键字ReentrantLock是JDK的类synchronized自动释放锁ReentrantLock需要手动加锁解锁finally里unlockReentrantLock支持可中断、可超时、公平锁、多条件队列Conditionsynchronized只能非公平竞争JDK 8之后synchronized性能已经和ReentrantLock打得有来有回通常建议优先用synchronized只有需要高级特性时才用ReentrantLock。8.3 ThreadLocal原理与内存泄漏风险ThreadLocal是面试中一致性很强的考点因为它既有原理又有实际踩坑。它的作用是让每个线程拥有自己的变量副本互不干扰。原理一句话就能讲清楚每个Thread内部有一个ThreadLocalMapkey是ThreadLocal对象弱引用value是你存的值。你调用ThreadLocal.set(value)其实是往当前线程的ThreadLocalMap里写数据get则是从当前线程的map里拿。那内存泄漏是怎么回事因为ThreadLocalMap的key是弱引用如果外部强引用被置空key就会被GC回收变成null但value还有一条引用链Thread → ThreadLocalMap → Entry → valuevalue永远不会被回收。如果线程是长期存活的比如线程池就会累积泄漏。解决方案每次用完ThreadLocal调用remove()或者在finally块里remove。这个习惯我在团队里反复强调但确实很多人会忘最后线上的内存告警给了教训。面试时你把这段讲出来面试官会觉得你真的是在项目中用过而不是背概念。9. Java新特性与函数式编程别让面试官觉得你落伍了9.1 Lambda表达式的本质与使用场景Lambda表达式是Java 8最重要的特性之一面试官通常会问Lambda的本质是什么答案是函数式接口的匿名实现类的简洁写法。所谓函数式接口就是只含一个抽象方法的接口比如Runnable、Comparator、Predicate、Function、Consumer、Supplier。Lambda可以引用外部变量但要求该变量是final或effectively final编译期就能确定不可变。为什么有这个限制因为Lambda可能在其他线程执行外部变量必须保证不可变才能避免数据竞争。这个规定初看是限制细想是避免并发问题的一种设计选择。面试官如果考方法引用你要能解释ClassName::method这种写法本质是把一个已有方法作为Lambda实现传递。比如list.stream().map(String::toUpperCase)等价于s - s.toUpperCase()。9.2 Stream API的中间操作与终止操作Stream是Java函数式编程的核心面试题经常是给定一个对象列表怎么用Stream实现过滤、分组、求和。你需要熟练掌握这些中间操作filter过滤、map转换、flatMap扁平化、sorted排序、distinct去重、limit截断、skip跳过终止操作collect收集、forEach遍历、reduce归约、count计数、anyMatch/allMatch/noneMatch匹配判断Stream的操作是惰性的中间操作不会立即执行只有遇到终止操作才会真正执行。这个惰性求值机制是Stream性能的关键它能实现短路比如limit(10)配合filter只要找到10个满足条件的就停止不会遍历整个集合。实际使用中最常见的坑是Stream的线程安全问题。用并行流parallelStream时如果用线程不安全的集合收集结果或者共享可变状态就会出问题。我的建议是数据量不大别用并行流创建线程池的开销可能大于收益数据量大的场景优先用parallelStream配Collectors让框架去处理线程安全。9.3 记录类型Record和密封类Sealed ClassJDK 14推出了recordJDK 17转正。它不是语法糖那么简单。一个record类自动生成构造方法、equals、hashCode、toString。在写不可变数据传输对象时非常省事。比如我写一个Result对象用record定义public record Result (int code, String message, T data) { }所有样板代码JVM帮你生成。record的字段天生final只有getter没有setter天然不可变。这一点在并发编程里很占便宜不可变对象线程安全。密封类sealed class是一个更抽象的语法用sealed修饰类用permits列出允许的子类其他类不能继承它。它和final的区别是final完全封闭sealed是受限开放。这在领域建模时特别有用比如一个Animal类只允许Cat和Dog继承其他都不行编译器层面保证了穷尽性。面试官如果问到你能说出sealed class 让类型系统更精确配合switch模式匹配可以做到穷尽性检查那基本就是加分了。10. 面试复习路线与实战建议10.1 三个月从零开始怎么安排时间如果你现在是一个突击面试的状态时间有限我建议把复习分三个阶段。第一阶段约2周主攻面向对象、String、集合框架、异常。这四块是基础中的基础任何面试都躲不开。目标是能画清楚内存结构图、能默写HashMap put流程、能讲清equals和hashCode的关系。先走一遍理解为主别背。第二阶段约3周主攻JVM内存模型、GC、并发基础、反射与注解。这些内容比较烧脑建议配合画图和demo验证。每一个原理你都要亲手写个小程序看输出比如写一个死循环线程观察栈溢出写一个往List里不断塞对象观察堆OOM。动手验证过的知识记忆特别牢。第三阶段约4周主攻新特性、Spring核心原理、数据库索引、事务、锁、Redis和消息队列这部分已经跨到基础之外了但Java基础面试到后半程一定会往框架和中间件延伸。你至少得知道Spring的Bean生命周期、MySQL的InnoDB索引结构、Redis的持久化机制否则基础答得再漂亮框架环节掉链子也过不了。10.2 面试时怎么表达才能拿高分技术面试不仅是考你会不会还考你会不会说。同样的答案表达方式不同分数差别很大。我的经验是三步法先给结论再讲原理最后举例子。比如面试官问HashMap为什么线程不安全你先说并发put时可能出现数据覆盖然后讲原理两个线程同时定位到同一个桶都执行了尾插操作后一个覆盖了前一个最后举例如果两个线程同时put 1和2结果可能是只有2没有1。这个结构逻辑清晰面试官听着也省力。还有一个细节遇到不会的问题别直接说不会。先说出你能确定的相关概念然后把问题拆成你能理解的部分。比如面试官问一个偏门的JVM参数你可以说这个参数的细节我没实际配过但我知道和它相关的另一个参数是XX我在某个场景下用它解决了什么问题。这比死撑着编要体面得多也给了面试官一个判断你学习能力的窗口。情绪管理也是实力的一部分。我见过不少候选人因为一个题没答上整个人慌掉了后面的题全部发挥失常。正确的做法是把不会的题当成一次探讨机会心态放平能接几轮就接几轮面试本来就是双向了解的过程。10.3 基础扎实之后往哪里深入如果你基础部分的八股已经刷得滚瓜烂熟接下来应该往三个方向深入它们才是面试分水岭。第一个方向是框架源码。Spring的Bean生命周期、事务传播机制、MyBatis的SQL执行流程、SpringBoot的自动配置原理。这些框架是Java开发的日常面试官必然会问而且会追问底层机制如果你只懂怎么用很容易被问到哑口无言。第二个方向是分布式与微服务。Redis为什么这么快、Kafka怎么保证消息不丢失不重复、分布式事务怎么实现、注册中心与配置中心的作用。这些是两年以上Java岗的必备知识也是基础扎实后最自然的延伸。第三个方向是性能调优与线上排查。GC日志怎么分析、慢SQL怎么定位、接口耗时怎么拆分、系统OOM怎么应对。这个方向最能体现实操经验也是从高级工程师迈向架构师的必经之路。我在面试候选人时只要对方能拿一个真实的线上故障讲清楚排查过程无论岗位要求是什么我都会给高分。说到底Java基础面试考的不是记忆力的极限而是你对一门编程语言的理解深度。真正的老手不是背得最多的人而是踩过最多的坑、总结出最多规律的人。复习时多问自己几个为什么多动手写几段demo验证比抱着题库刷一百遍管用得多。最后再分享一个小技巧面试前一周把每个核心知识点都写在一张卡片上正面是问题背面是结论原理示例三段式答案每天睡前抽十张口述作答模拟面试节奏。这个习惯我用了很多年比埋头看文档效率高一倍。
返回列表