面试模块:15 道题,接住下一问
record 和普通类、Lombok 生成的类有什么本质区别?
record 是语言规范里的透明数据载体:编译为 final class,隐式继承 java.lang.Record,组件变成 private final 字段和同名访问器;equals / hashCode / toString 由编译器按全部组件生成,字节码里通过 invokedynamic 链接到 ObjectMethods。
它不能声明额外实例字段,也不能继承其他类;可以实现接口,可以有静态成员和额外方法。构造器分规范构造器和紧凑构造器:后者省略参数列表,只写校验或归一化。
和 Lombok 的区别在于:record 的语义由语言规范固定,可被反射识别(isRecord() / getRecordComponents()),也能作为模式匹配的载体;Lombok 只是编译期生成普通类,字段可变性、继承和生成策略都可配置。
紧凑构造器和规范构造器有什么区别?为什么紧凑构造器里不能写 this.x = x?
规范构造器的参数列表与组件完全一致;紧凑构造器省略参数列表,参数名与组件同名,编译器在构造器末尾自动插入字段赋值。
在紧凑构造器里,x = ... 是在给隐式参数赋值,最终由编译器写回字段;字段本身是 final,且在构造器体里尚未由你赋值,所以 this.x = x 会报 cannot assign a value to final variable。
紧凑构造器适合参数校验、范围归一化和防御性拷贝。它不应做 I/O、查库或昂贵计算;构造器应该快速失败,而不是产生副作用。
record 能做什么、不能做什么?它适合当 JPA 实体吗?
record 可以实现接口、声明静态字段和静态方法、定义泛型/局部/嵌套 record,也可以重写访问器和 equals/hashCode/toString。
它不能声明额外实例字段、不能继承其他类、不能变成非 final;没有无参构造器,也没有 setter 语义。需要可变状态、代理或复杂生命周期的对象不适合。
JPA 实体通常需要无参构造器、可变的字段和代理,record 与之冲突;DTO、查询结果、事件、Map 复合 key 才是它的主场。
sealed 的 permits 有哪些约束?它和 final、enum 是什么关系?
sealed 限制谁可以继承/实现:直接子类型必须在 permits 中列出(同一源文件内可省略),且每个子类型必须声明为 final、sealed 或 non-sealed。
sealed 类型在命名模块中时,子类型必须同模块;在未命名模块(classpath)中时,必须同包。它描述的是封闭的继承体系,不是访问控制。
final 关闭继承,sealed 允许受控扩展,non-sealed 重新打开继承。enum 天然是 final 且常量集合固定,可以实现 sealed 接口;record 也天然 final,同样可以作为 sealed 的叶子。
JDK 17 里 sealed 能让 switch 不写 default 吗?
不能直接做到。17 的普通 switch 仍然只接受常量 case;要按 sealed 的子类型分支并使用穷尽性检查,需要 JEP 406 的 switch 类型模式,而它在 17 是预览。
使用预览时,编译和运行都要 --enable-preview;编译器会检查所有 permitted 子类型是否被覆盖。守卫条件不计入穷尽性,所以带守卫的分支后面仍要有无守卫的兜底分支。
21 起 switch 模式匹配正式(JEP 441),sealed + 穷尽 switch 才成为稳定写法。17 上把 sealed 说成「自动穷尽」是版本错误。
case Circle c && c.r() > 0;when 是 19 才改的。record pattern 在 17 完全不存在,不能写 case Circle(double r)。if (o instanceof String s && s.length() > 0) 中 s 为什么能用?换成 || 为什么不行?
模式变量采用流作用域:编译器分析控制流,只在确定匹配成功的路径上让变量可见。&& 右侧只在左侧为 true 时求值,因此 s 一定已绑定。
|| 右侧在左侧失败时也会求值,此时 s 未绑定,所以不可见。同理,if (!(o instanceof String s)) return; 之后的代码可以使用 s,因为能走到那里就说明匹配成功。
这不是简单的块作用域,而是结合布尔短路、break/continue/throw 的控制流分析。
JDK 17 的 switch 类型模式是什么状态?写法和 21 有什么不同?
17 的 switch 类型模式是 JEP 406 预览:selector 可以是引用类型,case 支持类型模式、case null 和守卫;编译器可以对 sealed 层级做穷尽性检查。
编译要 --release 17 --enable-preview,运行要 --enable-preview;class 文件版本是 61.65535,不能拿到别的 JDK 版本上运行。17 不支持 record pattern,解构要等到 21。
17 的守卫语法是 case Circle c && c.r() > 0;JDK 19 的 JEP 427 才把守卫改成 when。case null, default 在 17 预览里已经可用,但仍需 --enable-preview。把 21 的稳定语法直接搬到 17 会编译失败。
MatchException 是 21 的类,17 没有;不要用 21 的运行时行为描述 17。17 的预览代码必须锁定 JDK 版本并重新编译。模式 switch 的分支顺序和穷尽性是怎么判定的?
switch 从上到下匹配,第一个命中的分支获胜。更具体的模式必须放在更宽泛的模式之前,否则后面的分支不可达,编译器报 dominance 错误。case Object o 会吞掉后面所有类型分支。
穷尽性只对封闭类型有意义:sealed 接口要求覆盖所有 permitted 子类型,enum 要求覆盖所有常量(或提供 default)。守卫不计入穷尽性:带守卫的 Circle 分支不能证明覆盖了 Circle。
省略 default 的穷尽 switch 在编译期受保护;但二进制不兼容的类路径可能让运行时出现编译期未见的子类型,所以不要依赖运行期兜底。
case null 应该放在什么位置?case null 与 case null, default 只在预览 switch 中可用;普通 switch 选择器为 null 仍抛 NPE。17 的守卫用 &&,不是 19 起的 when。文本块的缩进和附带空白是怎么计算的?
文本块用 """ 包围,开分隔符后必须换行。编译器计算所有非空内容行与结束分隔符行的最小缩进,把这个附带空白从每行去掉,保留相对缩进。
结束分隔符的位置会影响最小缩进:它比内容更靠左时,内容会保留多余空格;它单独占一行时,结果末尾带一个换行。行尾空格默认被去掉。
文本块仍然是 String,适合 SQL、JSON、HTML 等多行内容;它不是 raw string,Java 转义规则仍然生效。
文本块里的 \s、行尾 \ 和 formatted 分别怎么用?
\s 表示一个空格,用于保留会被剥离的行尾空格;行尾的 \ 表示不换行,把下一行接上来。\n、\t、\\ 等转义仍然有效。
文本块没有插值。需要占位替换时用 String.formatted(JDK 15 起)或 String.format;需要模板引擎时要另外引入。
JSON 里的反斜杠要再转义一层:想得到字面量 \n,源码里要写 \\n,否则文本块会先把它变成真实换行。
${} 插值;String Templates 在 21 预览、23 撤回,17 没有这个能力。JEP 403 改变了什么?--illegal-access=permit 在 17 还能用吗?
JDK 16 的 JEP 396 把默认策略改为拒绝,但保留 --illegal-access=permit 作为退路;JDK 17 的 JEP 403 移除了这个选项,JDK 内部包一律强封装。
17 上传入 --illegal-access=permit 只会得到警告并被忽略,反射访问 java.lang.String.value 仍会抛 InaccessibleObjectException。
处理顺序是升级库、替换内部 API,最后才用最小范围的 --add-opens。--add-opens 是运行时参数,不改变编译期可见性。
--add-opens 不能解决编译错误?sun.misc.Unsafe 等少数 API 仍由 jdk.unsupported 导出,但不是普遍例外。exports 和 opens 有什么区别?--add-exports 与 --add-opens 分别什么时候用?
exports 让其他模块在编译期和运行期访问包中的 public 类型;opens 允许运行期反射访问包中的非 public 成员,不改变编译期可见性。
--add-exports 用于编译期和运行期访问未导出的 public API;--add-opens 只用于运行期反射。两者都可以在 module-info 中声明,也可以用命令行参数临时放行。
目标写 ALL-UNNAMED 表示 classpath 上的未命名模块;命名模块要写具体模块名。jdeps 能找到静态内部 API 引用,但找不到纯反射访问。
--add-opens 不会让代码在编译期看到未导出的包;编译期仍需要 --add-exports 或 module-info 的 exports。G1 的 Region、Remembered Set 和 Mixed GC 分别解决什么问题?
G1 把堆划分成大小相等的 Region,年轻代只是 Region 的逻辑集合。Young GC 回收 Eden/Survivor,存活对象复制到新的 Region 或晋升老年代。
Remembered Set 记录跨 Region 引用,让 Young GC 只扫描有跨代引用的 Region,而不是整个老年代;代价是写屏障和额外内存。
Mixed GC 在年轻代回收之外,挑选垃圾比例高的老年代 Region 一起回收,避免一次性全堆停顿。并发标记使用 SATB 维持快照语义;Full GC 仍是失败时的兜底。
ZGC 和 Shenandoah 的实现路线有什么不同?17 上怎么选?
ZGC 用染色指针把标记信息放进引用,通过 load barrier 让应用线程参与标记与重定位,并发完成大部分工作;17 的 ZGC 是单代收集器,不支持压缩指针,内存足迹更高。
Shenandoah 用 Brooks 指针(对象头中的转发指针)实现并发 evacuation,读屏障检查转发状态;它同样追求低停顿,但每对象多一个指针字。
两者在 15 都已转正。选择要看堆规模、尾延迟要求、CPU/内存预算和发行版支持;通用服务先用 G1,只有证明瓶颈在 GC 停顿时才切换。低停顿不等于高吞吐。
从 8 / 11 升级到 17,最重要的检查项有哪些?
先统一运行时到 17,用 --release 17 编译并开启 -Xlint:removal;跑 jdeps --jdk-internals 与 jdeprscan --release 17 --for-removal,列出内部 API 和待移除 API。
清理 JVM 参数:CMS、MaxPermSize 会让 JVM 直接拒绝启动;--illegal-access 被忽略;UseBiasedLocking 在 17 只是警告,21 才拒绝。
处理强封装:升级依赖、替换 sun.* / jdk.internal.*,必要时才加最小范围 --add-opens。RMI Activation 已移除,Applet 已弃用,Security Manager 在 17 只是弃用(仍可安装,有警告),21 才默认禁用。
怎么刷这套题HOWTO
- 先按主题筛:一次只攻一个主题,避免题目切换导致理解断裂。
- 先自答再展开:用 30 秒说出结论 + 一条原因 + 一个例子。
- 重点看追问:追问模拟面试官的第二层问题,能接住才算真会。
- 最后看版本陷阱:同一 API 在不同 JDK 的状态差异,是最容易答错的点。
- 标记掌握:不确定的不要标记;隔天再复习一遍。
难度说明LEVELS
- 高频:面试中出现频率高,或属于 JDK 17 的核心卖点。
- 进阶:需要理解机制、版本边界或实现取舍,答好能拉开差距。