面试模块:15 道题,接住下一问
Lambda 和匿名内部类在编译产物、this 语义和捕获方式上有什么本质区别?
编译产物不同:匿名内部类会生成独立的 class 文件,构造器、字段和方法都在编译期确定;Lambda 被提取成一个私有静态方法,调用点变成 invokedynamic,实现接口的类由 LambdaMetafactory 在运行期首次链接时生成。
this 语义不同:匿名内部类有自己的实例,this 指向匿名类对象;Lambda 没有独立接收者,this 指向创建它的外部实例。这也是 Lambda 不能访问自身成员的原因。
捕获方式不同:两者都只能捕获 final/effectively final 的局部变量,但匿名类通过构造器参数和合成字段保存,Lambda 通过链接时传给生成类工厂、再存入 arg$ 字段。Lambda 的类名带 $$Lambda$,不要依赖。
getClass()、this、序列化行为都不同;依赖类名或实例身份的代码在换 JDK 后可能失效。Lambda 捕获的局部变量为什么必须是 final 或 effectively final?
Lambda 捕获的是值快照,不是变量槽。局部变量在方法返回后栈帧就销毁了;如果 Lambda 还能修改它,就必须把变量提升到堆上并引入同步,语义和性能都会变复杂。
effectively final 表示变量初始化后不再被赋值;不要求显式写 final。这样编译器可以把值安全地复制到生成类的字段里,Lambda 内外看到的值一致。
需要可变状态时,应该改设计:把状态放进对象字段、用 AtomicInteger,或让 Lambda 返回新值;用单元素数组绕过只是把共享可变状态藏起来,并行时更危险。
for (int i = ...) 的 i 在循环里被修改,不是 effectively final,直接捕获会编译失败。方法引用的四种形式是什么?重载时编译器如何选择?
四种形式:静态方法(Integer::parseInt)、绑定实例(System.out::println)、未绑定实例(String::length)、构造器(ArrayList::new)。绑定实例在创建方法引用时就会求值接收者,不是延迟调用。
方法引用在编译期绑定到目标类型:编译器用函数式接口的参数和返回值来选择重载。目标类型是 Function<Object,String> 时,String::valueOf 会选 valueOf(Object);目标类型不同,可能选到另一个重载。
如果多个重载都能匹配,编译器报 ambiguous;显式写参数类型、改用代码块 Lambda,或先赋值给明确类型的变量,都能消歧。
String::valueOf 用作 Function<Object,String> 时调用哪个重载?Stream 的中间操作和终止操作如何分工?惰性求值和短路分别依赖什么?
中间操作(filter/map/sorted 等)返回新的 Stream,只构建流水线,不读取元素;终止操作(collect/forEach/count 等)才向上游拉取数据。没有终止操作,中间操作不会执行。
惰性来自 Sink 链:每个元素只在终止操作拉取时依次经过各阶段,而不是先把整个集合处理完。短路来自终止操作或 limit:findFirst、anyMatch 拿到结果后停止向上游要数据。
sorted/distinct 是有状态操作,必须先缓冲足够的元素,因此会破坏短路;在无限流上使用它们可能永远不结束。
stream.peek(...) 后面没有终止操作,会打印吗?IllegalStateException;peek 不应承载业务副作用,它的执行次数可能被优化。parallel stream 在什么条件下才值得用?它的线程池和风险是什么?
并行流用 Spliterator 切分来源,交给 ForkJoinPool.commonPool() 执行,默认并行度是 CPU 核数减一。它适合数据量大、CPU 密集、无状态、满足结合律且不依赖遇到顺序的操作。
不适合小数据量、I/O 密集、共享可变状态、强顺序依赖的任务;forEach 不保证顺序,findFirst 在并行流上保留顺序的成本较高,不需要顺序时用 findAny。
commonPool 是 JVM 共享的,阻塞任务会占住工作线程,拖慢其他并行流;在并行流里对 ArrayList add 或对普通计数器 ++ 会丢更新,应该用 collect/reduce 或线程安全结构。
ArrayList 里 add 会发生什么?Collections.synchronizedList 能避免结构损坏,但每个元素都加锁,通常比顺序流更慢。Collectors.toMap 和 groupingBy 在重复键、null 值上分别是什么行为?
toMap 默认用 HashMap,重复 key 会抛 IllegalStateException: Duplicate key ...;可以传 merge 函数决定保留前者、后者或合并。它的 value 不能为 null,因为内部用 Map.merge,会抛 NPE。
groupingBy 把重复 key 的元素收进同一个 List,默认 value 是 ArrayList;key 不能为 null(JDK 8 实测抛 NPE)。可以用 mapFactory 指定 TreeMap/LinkedHashMap,用下游收集器做 counting/summing。
需要保留插入顺序时用 LinkedHashMap::new;需要排序时用 TreeMap::new。不要把 Collectors.toList() 的结果当成不可变集合。
toMap 在重复键时保留后者?toMap 的 value 不能为 null,哪怕 HashMap 本身允许 null 值;这是 Map.merge 的实现细节。Optional 应该用在哪里?为什么不适合作为字段、方法参数和集合元素?
Optional 最适合可能缺失的返回值:它让调用方在类型上看到「可能没有值」,必须显式处理。它不是 null 的通用替代,也不是判空工具。
字段和参数会强迫调用方拆箱,增加序列化、RPC、ORM 的兼容问题;Optional 不实现 Serializable,放进 DTO 或远程模型会出问题。集合元素则应该用空集合表达「没有元素」,而不是 Optional。
Optional 还会增加对象分配;在性能敏感的热点路径上,直接返回 null 或使用特化的 OptionalInt 更合适。
Optional.of(null) 直接 NPE;get() 在空时抛 NoSuchElementException;Optional 不实现 Serializable,不要放进 DTO。orElse 和 orElseGet 的区别是什么?什么情况下会写出 bug?
orElse(T other) 的参数是普通表达式,在调用方法前就求值,不管 Optional 是否为空;orElseGet(Supplier) 只在为空时调用 Supplier。
默认值是常量时两者差别不大;默认值需要查库、创建对象、记录日志时,orElse 会在 Optional 有值时也执行,产生无谓副作用甚至错误。
需要抛异常时用 orElseThrow(Supplier);JDK 8 必须传 Supplier,无参版本是 JDK 10 才加入的。
map 和 flatMap 在 Optional 里怎么选?orElse 的参数在调用前就求值;把 orElseGet 写成 orElse(getDefault()) 是最常见的 bug 之一。java.time 相比 Date/Calendar 做了哪些设计修正?为什么它是线程安全的?
java.time 的核心类都是不可变的:修改操作返回新对象,原对象不变,因此天然线程安全,可以自由共享。Date 和 Calendar 是可变的,SimpleDateFormat 也不是线程安全的。
类型分工更清晰:Instant 表示时间线上的瞬时点,LocalDate/LocalTime/LocalDateTime 表示无时区的人类时间,ZonedDateTime 带时区与 DST 规则,Duration 表示精确时长,Period 表示日期跨度。
设计细节也修正了:月份从 1 开始而不是 0,年份不再减 1900,默认 ISO-8601,解析严格校验,DateTimeFormatter 不可变且可复用。
DateTimeFormatter 可以共享,而 SimpleDateFormat 不行?java.sql.Date.toInstant() 在 JDK 8 抛 UnsupportedOperationException;它只表示日期,应改用 toLocalDate()。LocalDateTime、Instant、ZonedDateTime 分别表示什么?跨时区存储应该用哪个?
LocalDateTime 是日期 + 时间,没有时区,不对应唯一瞬时点;Instant 是时间线上的点,以 UTC 纪元为基准;ZonedDateTime 是 LocalDateTime + ZoneId + 偏移量,带完整的时区与 DST 规则。
跨时区存储优先用 Instant(或 epoch millis),展示时再套 ZoneId;如果业务必须保留原始时区,就同时存 Instant 和 ZoneId,而不是只存 LocalDateTime。
LocalDateTime 适合「墙上时间」的业务规则,例如每天 9 点开门;它不能直接 toInstant(),必须先给时区。
ZonedDateTime.of 会怎么处理?LocalDateTime 不是 UTC;ZoneId.systemDefault() 会让测试依赖机器;DST 重叠时间需要显式选择 earlier/later offset。HashMap 的 hash 扰动、容量、扩容和树化条件分别是什么?
hash 扰动是 (h = key.hashCode()) ^ (h >>> 16),把高 16 位异或到低位;下标是 (n - 1) & hash,因此容量必须是 2 的幂。默认容量 16、负载因子 0.75,阈值 12。
元素数超过阈值时容量翻倍,JDK 8 用 (e.hash & oldCap) == 0 把旧桶拆成 lo/hi 两条链,分别留在原下标或移到原下标 + oldCap,不需要重新计算 hash。
树化条件:桶内节点超过 8(实际第 9 个插入触发 treeifyBin)且容量至少 64;容量不足 64 时先扩容。树化后查找 O(log n),扩容拆分时树节点不超过 6 会退回链表。
JDK 8 的 HashMap 在并发 put 下会发生什么?JDK 7 的扩容死循环在 8 里还有吗?
JDK 7 的扩容用头插法,两个线程同时 resize 可能把链表接成环,导致 get 死循环。JDK 8 改成尾插并保留 lo/hi 顺序,环形链表基本消失。
但 JDK 8 的 HashMap 仍然不是线程安全的:并发 put 会丢更新、覆盖彼此的结果、size 不一致,get 可能读到旧值;modCount 的 fail-fast 只是尽力检测。
并发场景用 ConcurrentHashMap;JDK 8 用 CAS + synchronized 锁桶,不再用 Segment 分段锁,锁粒度更细。需要复合操作原子性时用 computeIfAbsent/compute/merge。
Collections.synchronizedMap 的复合操作仍需手动同步;CHM 不允许 null key/value。PermGen 为什么被移除?Metaspace 在分配位置、上限和参数上有什么不同?
PermGen 是堆内一块固定大小的区域,存放类元数据、常量池等;大小难以预估,容易出现 OutOfMemoryError: PermGen space,而且 Full GC 才能回收。JEP 122 在 JDK 8 用 Metaspace 取代它。
Metaspace 使用本地内存,默认不设上限,受机器内存限制;类元数据在类加载器卸载时回收。-XX:MetaspaceSize 是首次 GC 的高水位,不是初始分配大小;-XX:MaxMetaspaceSize 设置硬上限,CompressedClassSpaceSize 限制压缩类指针区域。
Metaspace 无上限不等于不会 OOM:class loader 泄漏会持续占用本地内存,最终触发 OutOfMemoryError: Metaspace。
-XX:MaxPermSize=256m 只是警告?-XX:MaxPermSize 在 8 打印 ignoring option 后继续启动;9+ 变成 Unrecognized VM option,直接失败。Metaspace OOM 如何定位?常见的 class loader 泄漏有哪些?
先确认上限与现状:-XX:MaxMetaspaceSize、jstat -gcmetacapacity、jmap -clstats 查看类加载器与类数量;需要更细的类直方图时用 jcmd <pid> GC.class_stats(JDK 8 需要 -XX:+UnlockDiagnosticVMOptions)。
常见泄漏:应用服务器热部署后旧 class loader 未释放、线程池/ThreadLocal 持有应用类、JDBC 驱动注册未注销、动态代理/脚本引擎反复生成类、defineClass 后没有释放引用。
修复方向是断开旧 class loader 的 GC Root 引用,而不是简单调大 MaxMetaspaceSize;调大只是推迟 OOM,不能解决泄漏。
jmap -clstats 在 JDK 8 可用;jcmd VM.metaspace 是更高版本的工具,不要混用。MaxMetaspaceSize 只是上限,不解决泄漏。从 7 升到 8,哪些地方最容易出兼容性问题?default 方法真的完全二进制兼容吗?
最容易出问题的是运行时假设:-XX:MaxPermSize 被忽略、字节码库不认识 class 52、-source 1.7 残留导致 Lambda 编译失败、java.sql.Date.toInstant() 抛 UOE、Nashorn 被误当成长期依赖。
default 方法让接口可以在不破坏实现类的情况下演进:新增 default 方法不会强迫实现类重编译。但它不是绝对安全:新增抽象方法会破坏实现类;如果父类已有同签名但返回类型不兼容的方法,链接期仍可能失败。
升级顺序:统一 JDK 与工具链 → 清理 PermGen 参数 → 升级 ASM/CGLIB → 处理 default 冲突 → 迁日期 API → 逐步引入 Lambda/Stream。
MaxPermSize 在 8 只是警告,升到 9+ 才变成启动失败;JDK 8 没有 --release,不能用它检查 API 边界。怎么刷这套题HOWTO
- 先按主题筛:一次只攻一个主题,避免题目切换导致理解断裂。
- 先自答再展开:用 30 秒说出结论 + 一条原因 + 一个例子。
- 重点看追问:追问模拟面试官的第二层问题,能接住才算真会。
- 最后看版本陷阱:同一 API 在不同 JDK 的状态差异,是最容易答错的点。
- 标记掌握:不确定的不要标记;隔天再复习一遍。
难度说明LEVELS
- 高频:面试中出现频率高,或属于 JDK 8 的核心卖点。
- 进阶:需要理解机制、版本边界或实现取舍,答好能拉开差距。