语法、类型系统与异常处理

共 99 题
📑 题目列表 99 题
#
★★★

1. CompletableFuture 链中的 CompletionException 应在哪一层解包,才能避免重复包装和错误重试

在 CompletableFuture 的异步链式调用中,异步任务抛出的异常会被包装成 CompletionException,请问应该在链路中的哪一层解包该异常,才能避免异常的重复包装和错误的幂等重试?

  • CompletionException 与 ExecutionException 的包装机制
  • CompletableFuture 链式调用中异常传播与解包的边界
  • 重试与幂等策略对异常处理层级的要求

解包应集中在链路的"边界层"(即任务真正开始的入口,如最外层异步任务的回调或调度器),而不是在每个中间阶段就不断 get() 并解包。CompletionException 在 whenComplete/handle 等回调里拿到的是链上某个阶段抛出的原始异常被包装后的结果;如果每个中间阶段都解包再重新包装,会造成嵌套的 CompletionException 并丢失原始栈。正确做法是在任务入口捕获并解包一次,把业务异常(如领域异常)转换为带错误码的领域异常再向上传播,同时保留 cause 链;重试只对明确可重试的异常(如临时网络故障、限流)进行,且幂等键应放在被重试的业务操作上,而不是在解包层盲目重试。

CompletionException 是 JDK 为异步完成阶段引入的受检异常包装,多次 join/get 会得到同一实例但可能被再次包装。在链的最外层统一解包,可保证异常只被解包一次,避免重复包装破坏原始 cause 与栈,从而让重试判定基于根因而非包装层。

CompletableFuture<Order> f = CompletableFuture.supplyAsync(() -> remoteSubmit(order));
f.handle((result, ex) -> {
    if (ex == null) return result;
    Throwable cause = (ex instanceof CompletionException && ex.getCause() != null)
        ? ex.getCause() : ex;           // 只在这一层解包一次
    if (isRetryable(cause)) {
        return retryWithSameIdempotencyKey(order); // 幂等重试
    }
    throw new BizException(ErrorCode.REMOTE_FAIL, cause); // 稳定错误码
}).join();
#
★★★

2. JVM 如何对异常做栈展开(Stack Unwinding)的成本

JVM 在抛出异常时如何进行栈展开(Stack Unwinding),这个过程的主要成本是什么?

  • 异常抛出时的栈展开机制
  • 栈展开与异常捕获的判断成本
  • 异常在高频路径上的性能影响

当异常被抛出且未被当前方法捕获时,JVM 会沿调用栈逐帧向上查找匹配的异常处理器(catch 块),找到后让栈帧出栈并跳转到处理器。栈展开的成本主要包括:逐帧扫描异常表(Exception Table)匹配异常类型与行号、栈帧的销毁与局部变量表清理、以及一旦匹配失败继续向上传播。现代 HotSpot JVM 对"try 块内未抛异常"的路径做了优化(异常表只在真正抛异常时才线性扫描),因此正常的 try/catch 几乎零成本;成本主要发生在抛异常本身——fillInStackTrace 会遍历栈帧生成 StackTraceElement 数组,这是最主要的分配与 CPU 开销。

栈展开本身是"按需"的,JIT 会利用异常表做精确匹配。真正昂贵的是异常对象的创建与栈快照捕获,所以高频路径应避免用异常做控制流,而应先用返回值或 Optional 判断。

// 高频校验路径:用返回值/Optional 而非异常控制流
if (user == null) return Optional.empty();   // 避免异常
if (list == null) return List.of();          // 避免 NPE 异常栈开销
#
★★★

3. JVM 异常表(Exception Table)的字节码原理

请解释 JVM 异常表(Exception Table)的字节码原理,它是如何支持 try/catch 的实现的?

  • 异常表的结构与字段
  • 字节码层面 try/catch 的实现
  • 异常匹配与 finally 的处理

编译后的方法在 Class 文件里带有一张异常表(Exception Table),每条记录包含 start_pc、end_pc、handler_pc 和 catch_type。前两者表示 try 块覆盖的字节码范围,handler_pc 是 catch 处理器入口,catch_type 表示要捕获的异常类型(0 或 null 表示 finally/any)。当方法某条指令在 [start, end) 范围内抛出异常时,JVM 按异常表顺序查找第一个 catch_type 与该异常类型匹配(或子类)的条目,跳到 handler_pc 执行;finally 块通过复制代码或编译器生成额外的异常表条目,保证无论正常返回还是异常都执行清理。异常匹配是"从内向外、按声明顺序"进行,因此更具体的 catch 应放在前面。

异常表是编译期生成的静态元数据,运行时抛出异常时按表线性扫描找到处理入口,这正是 try/catch 的字节码本质。这也是为什么 Java 的 try/finally 会被编译成多条异常表的条目,代价是异常路径上代码复制。

// 源码:
try {
    risky();
} catch (IOException e) {
    log(e);
} finally {
    cleanup();
}
// 编译后:异常表含 catch IOException 与 finally(any) 两条目
// catch IOException -> handler_pc 指向 log(e)
// any -> handler_pc 指向 cleanup() 后重抛
#
★★★

4. OutOfMemoryError 在 G1 与 ZGC 下的堆转储(Heap Dump)触发行为与捕获建议

在 G1 与 ZGC 垃圾收集器下,OutOfMemoryError 触发堆转储(Heap Dump)的行为有何不同,应该如何捕获并处理这类错误?

  • -XX:+HeapDumpOnOutOfMemoryError 的触发机制
  • G1 与 ZGC 在 OOM 时的行为差异
  • OOM 的捕获与诊断建议

-XX:+HeapDumpOnOutOfMemoryError 会在堆分配失败抛出 OOM 时生成 hprof 堆转储文件(配合 -XX:HeapDumpPath 指定路径),用于事后分析。G1 与 ZGC 都遵循这一机制,但触发的具体场景略有差异:G1 在无法完成 GC 后仍无法分配满足晋升或分配失败时抛 OOM;ZGC 是并发回收,通常在 GC 后仍无法分配时才抛 OOM(ZGC 更倾向通过内存回收报告而非立刻 OOM)。由于 ZGC 使用并发整理、几乎没有停顿,堆转储的快照质量与标记细节不同。捕获建议:不要依赖 catch 子句恢复(OOM 本身不可恢复),应保留现场(dump + core + JVM 日志 + GC 日志),并设置 -XX:+ExitOnOutOfMemoryError 快速失败交由容器重启,避免在近乎耗尽内存的环境里继续执行可能挂死。

无论哪种收集器,OOM 都意味着堆无法满足分配,此时继续执行极不稳定。堆转储是诊断工具而非恢复手段,正确姿势是让进程快速退出并转储,由编排层拉起新实例。

// JVM 启动参数示例
// -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dumps
// -XX:+ExitOnOutOfMemoryError -Xlog:gc*:file=/data/gc.log:time
// 代码中不应 catch(OutOfMemoryError) 后恢复,只应记录并退出
#
★★★

5. Spring Boot 4.0 如何用 ProblemDetail 统一映射领域异常,同时区分客户端错误与服务端故障

Spring Boot 4.0 中如何用 ProblemDetail(RFC 7807)统一映射领域异常,并在映射过程中区分客户端错误与服务端故障?

  • Spring 6 引入、Boot 4 沿用的 ProblemDetail 与 ErrorResponse 机制
  • @ControllerAdvice 与 @ExceptionHandler 的使用
  • 4xx 与 5xx 状态码的区分

Spring Boot 4(基于 Spring Framework 7)提供 ProblemDetail 与 ErrorResponse 来遵循 RFC 7807 标准错误响应格式(该机制自 Spring 6 起引入)。在 @ControllerAdvice 中,用 @ExceptionHandler 为不同异常生成 ProblemDetail,每个 ProblemDetail 包含 type/title/status/detail/instance 及自定义扩展属性。区分客户端错误与服务端故障的核心是异常类型与状态码的选择:客户端错误(参数校验失败、资源不存在、校验失败)映射为 4xx(如 400/404/422),并携带稳定的错误码与可读提示;服务端故障(数据库异常、远程调用失败、未知异常)映射为 5xx(如 500/503),仅暴露通用信息而不泄露堆栈与内部实现细节。通过 ErrorResponseException 或 ProblemDetail 的 status 字段统一管理,便于前端与网关按状态码分流。

ProblemDetail 是标准化的错误响应结构,把"错误码/提示/详情"结构化,避免自定义错误 DTO 的混乱。区分 4xx/5xx 的本质是错误的责任归属:客户端错误不重试,服务端错误可重试并需告警。

@ControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(ValidationException.class)
    ProblemDetail handleValidation(ValidationException e) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(
            HttpStatus.BAD_REQUEST, e.getMessage());       // 4xx 客户端错误
        pd.setTitle("Validation Failed");
        pd.setProperty("errorCode", e.getCode());
        return pd;
    }
    @ExceptionHandler(RemoteCallException.class)
    ProblemDetail handleRemote(RemoteCallException e) {
        ProblemDetail pd = ProblemDetail.forStatusAndDetail(
            HttpStatus.SERVICE_UNAVAILABLE, "remote unavailable"); // 5xx 服务端故障
        pd.setTitle("Service Unavailable");
        return pd; // 不泄露内部堆栈
    }
}
#
★★★

6. String 不可变性的设计意义与 JIT 优化(String Constant Pool)

String 不可变性有哪些设计意义,它如何支撑 JIT 优化与字符串常量池(String Constant Pool)?

  • String 不可变性的安全性与线程安全意义
  • 字符串常量池的作用
  • JIT 对不可变字符串的优化

String 被设计为不可变(final 类、char[] 为 final 且不暴露),核心意义包括:线程安全(可被多个线程无锁共享)、哈希值可缓存(首次计算后即永久不变)、安全(可安全作为 HashMap 键、类名、网络传输类型而不被篡改)、以及常量池复用。字符串常量池(String Constant Pool)允许相同内容的字面量字符串在 JDK 7+ 位于堆中并由 StringTable 维护,编译期即可复用同一对象。JIT 与编译器基于不可变性做优化:常量折叠、重复子串/字符串拼接的优化、字符串比较的快速路径、以及 GC 扫描。本质上不可变性让字符串对象引用成为可安全共享的"值对象",从而支撑哈希缓存、常量池去重与并发安全等一系列优化。

不可变性是字符串作为"值"的基石,它既保证了语义安全(值不会被改变),又为性能优化(哈希缓存、常量池、JIT 常量传播)提供了前提。

String a = "hello";
String b = "hello";
System.out.println(a == b);      // true,常量池中复用同一对象
String c = new String("hello");
System.out.println(a == c);      // false,new 创建新对象
System.out.println(a.equals(c)); // true,值相等
#
★★★

7. Unicode 与 UTF-16 在 JVM 中的字符存储(char/code point)

JVM 中 Unicode 字符是如何存储的?char 与 code point(码点)有何区别?

  • Unicode 与 UTF-16 编码
  • char(UTF-16 code unit)与 code point 的关系
  • 代理对(surrogate pair)与补充平面字符

JVM 内部用 UTF-16 表示字符。char 是 16 位的 UTF-16 code unit(一个编码单元),基本平面(BMP,U+0000~U+FFFF)中的字符占一个 char;而补充平面(U+10000~U+10FFFF)的字符需要两个 char 组成一个"代理对"(surrogate pair,高代理 0xD800~0xDBFF + 低代理 0xDC00~0xDFFF)。因此 char 的数量不等于 code point(码点)的数量:一个补充平面字符(例如 emoji)length() 为 2,但 codePointCount() 为 1。正确处理 Unicode 需要用 codePointAt、codePoints()、String.codePointCount 等 API,避免按 char 截断导致乱码或半个代理对。

理解了 char 是 UTF-16 code unit 而非 "字符",就明白为什么 substring 按 char 索引可能截断一半代理对,以及为什么需要码点级 API 处理非 BMP 字符。

String s = "A𝄞";                        // A + 音乐符号(补充平面)
System.out.println(s.length());          // 3  (2 char for 𝄞 代理对)
System.out.println(s.codePointCount(0, s.length())); // 2 码点
int cp = s.codePointAt(1);               // 0x1D11E
System.out.println(Character.isSurrogatePair(s.charAt(1), s.charAt(2))); // true
#
★★★

8. hashCode 与 equals 契约的细节、违反契约时集合行为

hashCode 与 equals 之间有哪些契约细节?违反契约时集合会表现为什么行为?

  • equals/hashCode 契约(consistent/reflexive 等)
  • 违反契约对 HashMap/HashSet 的影响
  • 可变对象作为键的问题

契约核心是:若 equals 为 true,则 hashCode 必须相等(反向不成立);equals 与 hashCode 必须一致(对同一对象多次调用结果不变);equals 需满足自反、对称、传递、一致与非空性。若违反(如"equals 相等但 hashCode 不同"),则对象会落入不同桶,导致 HashMap 中 equals 相等的对象被当作不同键,出现 containsKey 返回 false、get 返回 null、重复插入等错误。更隐蔽的是把可变对象作为键,修改参与 hashCode 的字段后,桶位置改变,后续 get/remove 在错误桶中无法找到,且对象永远留在集合中形成内存泄漏。正确做法是优先用不可变对象作键,或重写 hashCode 时只依赖不可变字段。

HashMap 先按 hashCode 定位桶,再在桶内用 equals 比较。桶维度靠 hashCode,桶内维度靠 equals,二者缺一不可,违反契约即在某层失效。

class BadKey {
    String id;
    public boolean equals(Object o){ return o instanceof BadKey b && b.id.equals(id); }
    // 缺 hashCode()  -> 违反契约,equals 相等但 hashCode 不同
}
// HashMap.containsKey(new BadKey("1")) 在错误桶中查不到,返回 false
#
★★★

9. 在 finally 中抛出异常的覆盖与栈丢失

在 finally 块中抛出异常会覆盖什么?为什么会造成原始异常栈丢失?

  • finally 中抛异常覆盖主异常
  • 栈丢失与 suppressed 机制
  • 如何避免 finally 抛异常

如果 try 块已经抛出异常,而 finally 块又抛出异常(或 finally 中 return 会吞掉 try 的异常),finally 抛出的异常会覆盖 try 中抛出的原始异常,导致调用方只看到 finally 的异常,原始异常的栈与 cause 丢失。JDK 7 引入的 try-with-resources 通过 addSuppressed 机制保留被抑制的异常(suppressed exceptions),但手写 finally 并不会自动保留。正确做法是:finally 中只做资源清理并吞掉或记录清理异常,不要在 finally 中抛出新异常;若必须抛出,应使用 addSuppressed 把原始异常附加为被抑制的异常(suppressed),以保留诊断信息。

覆盖的本质是"异常替换"——JVM 在 finally 中执行新异常抛出的 throw 时,会丢弃进行中的原始异常传播。suppressed 机制正是为记录这些被覆盖的异常而设计。

try (Closeable c = acquire()) {   // 推荐,自动 close 且保留 suppressed
    work();
}
// 手动写法:
Throwable primary = null;
try {
    work();
} catch (Throwable t) {
    primary = t;
    throw t;
} finally {
    try { cleanup(); }
    catch (Throwable t) { if (primary != null) primary.addSuppressed(t); throw t; }
}
#
★★★

10. 基本类型与包装类的差异、自动装箱拆箱的缓存范围与坑点

基本类型与包装类的差异是什么?自动装箱拆箱的缓存范围如何,有哪些坑点?

  • 基本类型与包装类的存储与性能差异
  • 自动装箱拆箱机制
  • 包装类缓存范围与 == 比较的坑

基本类型(int/long/boolean 等)直接存值,无对象开销、性能高;包装类(Integer/Long/Boolean 等)是对象,存于堆,提供 null 表示、泛型支持与工具方法。自动装箱(boxing)与拆箱(unboxing)由编译器在赋值/运算时自动插入 Integer.valueOf 与 intValue 等调用。坑点主要来自缓存与 == 比较:Integer/Long/Short/Byte 缓存 -128~127,Character 缓存 0~127,Boolean 缓存 true/false;在此范围内 valueOf 返回缓存对象,== 比较相等,超出范围则 new 新对象,== 比较(引用相等)为 false。因此包装类之间比较应使用 equals 或 intValue,避免 == 的语义陷阱。另外包装类可能出现 null(拆箱 NPE)与在大循环中大量装箱带来的性能开销。

缓存让常用小值复用对象,是"== 对小值相等、大值不等"的根源。理解后应牢记:包装类比较用 equals,追求性能用基本类型或原始流。

Integer a = 100, b = 100;
System.out.println(a == b);  // true  (缓存范围)
Integer c = 200, d = 200;
System.out.println(c == d);  // false (超出缓存,new 新对象)
System.out.println(c.equals(d)); // true 正确比较
Integer n = null;
int x = n;                    // NPE:自动拆箱
#
★★★

11. 基本类型包装类的缓存范围(IntegerCache)在不同 JDK 版本下默认值与可配置边界如何

基本类型包装类的缓存范围(IntegerCache)在不同 JDK 版本下的默认值是多少?缓存边界是否可配置?

  • IntegerCache 的默认范围 -128~127
  • JDK 9+ 的 high 可配置性
  • 其他包装类的缓存范围

IntegerCache 默认缓存 -128~127,可通过 -XX:AutoBoxCacheMax 或系统属性 java.lang.Integer.IntegerCache.high 提升缓存上限(配置值低于 127 会被抬升回 127,高于 Integer.MAX_VALUE-129 会被钳制到该上限,防止数组越界)。Long 的缓存范围固定为 -128~127,不可配置;Short/Byte 缓存全部取值(Byte 与 Short 天然覆盖全部范围),Character 缓存 0~127,Boolean 缓存 true/false。较低 HIGH 值场景下,超出缓存范围的值每次 valueOf 都会 new 新对象,造成 == 比较失败与额外分配。

缓存范围是 -XX:AutoBoxCacheMax 可调的上限,但 Long 不可调。理解各包装类缓存边界,才能避免把"缓存命中"误当可靠语义。

// JVM 参数提升 Integer 缓存上限
// -XX:AutoBoxCacheMax=1000  (提升 Integer 缓存上限)
Integer a = Integer.valueOf(600);  // 若 high>=600 命中缓存
// Long 缓存固定 -128~127,不可配置
Long x = Long.valueOf(200L);       // 始终 new 新对象
#
★★★

12. 字符串拼接 + 与 StringBuilder/StringConcatFactory 的性能边界

字符串拼接使用 + 操作符与 StringBuilder/StringConcatFactory 相比,性能边界在哪里?

    • 拼接的编译实现(StringBuilder 或 invokedynamic)
  • StringConcatFactory 与 makeConcatWithConstants
  • 循环拼接的坑

编译期 + 拼接在 JDK 8 及以前会被编译成 new StringBuilder().append()...toString(),多段拼接(常量段)会触发常量折叠;JDK 9+ 默认使用 invokedynamic + StringConcatFactory,运行时用 makeConcatWithConstants 生成高效的拼接字节码,避免显式 StringBuilder 分配,且对常量段做预解析。两者的性能边界在于:单条语句内的多个 + 拼接(编译器能合并成一次操作)性能都很高;而循环内不断拼接(如"str += x")则每次迭代都会新建 StringBuilder/重新分配,复杂度退化为 O(n²),应改为循环外创建 StringBuilder 并在循环内 append。若拼接片段数量固定且量小,+ 即可;字符串很多或需要条件追加时显式 StringBuilder 更可控。

  • 的编译优化(StringBuilder 或 StringConcatFactory)让简单拼接高效,但循环中的累积拼接无法被优化,因为每次 += 都产生新对象,是明确的性能缺陷。
// 差:循环内 + 拼接,O(n²) 且每次新建对象
String s = "";
for (int i = 0; i < 10000; i++) s += i;
// 好:循环外 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) sb.append(i);
String result = sb.toString();
#
★★★

13. 自定义异常的命名、序列化与栈快照成本

自定义异常应如何命名、处理序列化,并考虑栈快照(stack trace)成本?

  • 自定义异常命名规范(后缀 Exception)
  • 序列化 serialVersionUID 与 finalize
  • fillInStackTrace 与栈快照成本

自定义异常应继承 Exception(受检)或 RuntimeException(非受检),命名以 Exception 结尾,提供多个构造器(无参、带消息、带 cause、带消息和 cause),若需序列化则显式声明 serialVersionUID 并保持字段可序列化。栈快照成本:默认构造异常时 fillInStackTrace() 会捕获完整调用栈,高频路径上昂贵;若不需要栈可重写 fillInStackTrace() 返回 this 或调用 super 不填充(置空栈),从而减少对象分配与栈遍历。命名与序列化属于工程规范,栈快照成本则是性能考量——异常创建应避免在热路径上发生。

自定义异常是类型的表达(区分业务错误与系统错误),构造器透传 cause 保留诊断链,序列化保证跨进程/跨版本稳定,栈快照成本决定是否可用无栈异常。

public class OrderException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    private final String code;
    public OrderException(String code, String message) { super(message); this.code = code; }
    public OrderException(String code, String message, Throwable cause) {
        super(message, cause); this.code = code;
    }
    public String getCode() { return code; }
    // 热路径可禁用栈快照
    @Override public synchronized Throwable fillInStackTrace() { return this; }
}
#
★★★

14. 虚拟线程执行的并发子任务分别失败时,调用方应如何聚合异常、传播取消并保留诊断上下文

使用虚拟线程执行的多个并发子任务分别失败时,调用方应如何聚合异常、传播取消并保留诊断上下文?

  • 虚拟线程与结构化并发(Structured Concurrency)
  • 异常聚合与取消传播
  • MDC/诊断上下文保留

推荐使用 JEP 453 结构化并发(StructuredTaskScope)来管理多个虚拟线程子任务:所有子任务在同一个作用域内,StructuredTaskScope 负责统一生命周期。当子任务分别失败时,可用自定义的 StructuredTaskScope 收集各子任务异常(在一个 field 里聚合),待全部子任务结束后统一抛出聚合异常;若希望快速失败,可在子任务 join 时捕获异常并调用 scope.shutdown() 取消其余任务,从而传播取消。虚拟线程并非线程池,传递 MDC 等诊断上下文需使用基于复制(copy)的机制(如 ThreadLocal 的 virtual 继承或显式传递),因为虚拟线程不会被线程池复用,j.u.c.ThreadLocal 的继承语义不再依赖线程池线程。保留诊断上下文的关键是把 traceId 等显式传入子任务或使用可复制的上下文载体。

结构化并发把"并发子任务"当作一个整体单元,失败时要么聚合要么取消,避免孤儿线程泄漏。虚拟线程背景下,诊断上下文不能依赖传统线程池继承,需显式传递或复制。

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> a = scope.fork(() -> callA());
    Future<String> b = scope.fork(() -> callB());
    scope.join().throwIfFailed();   // 任一失败则传播取消并抛聚合异常
    String r = a.resultNow() + b.resultNow();
} catch (Exception e) {
    // e 为聚合异常,包含失败子任务的 cause
}
#
★★★

15. 远程调用超时、连接失败和业务拒绝应如何分类,才能制定不同的幂等重试与熔断策略

远程调用中的超时、连接失败和业务拒绝应如何分类?如何据此制定不同的幂等重试与熔断策略?

  • 异常分类(可重试/不可重试;客户端/服务端)
  • 幂等重试与熔断策略
  • 超时判定与降级

应把远程调用异常分类为:可重试的瞬时故障(连接失败、超时、网络抖动的 5xx/限流)与不可重试的确定性失败(4xx 业务拒绝、参数错误、认证失败)。分类依据是失败的责任归属与重试是否可能成功:连接失败/超时多为瞬时,重试可能成功,但需幂等(幂等键)避免重复副作用;业务拒绝(4xx)是确定性的,重试只会再次失败,应直接失败并记录,不重试。基于分类制定策略:可重试异常用指数退避 + 抖动进行有限次重试(配合幂等键),并计入熔断器;连续失败触发熔断(快速失败),恢复走后端探测;不可重试异常直接短路不重试。超时需区分"请求未发出"与"已发出但结果未知",后者重试必须幂等。

重试与熔断的前提是"异常可重试且操作幂等"。分类决定是否重试(可重试瞬时故障)、是否幂等(已发出但未知结果)、是否熔断(连续失败),业务拒绝不重试。

if (isRetryable(ex)) {           // 连接失败/超时/5xx/限流
    retryWithBackoff(key, attempt+1);   // 指数退避 + 幂等键
} else if (isBusinessReject(ex)) {      // 4xx 业务拒绝
    return failFast(ex);                // 不重试,直接失败
} else {
    circuitBreaker.recordFailure();     // 累计失败,触发熔断
}
#
★★★

16. 频繁调用 Throwable.fillInStackTrace 会带来哪些分配与栈遍历成本,何时可采用无栈异常

频繁调用 Throwable.fillInStackTrace 会带来哪些分配与栈遍历成本?何时可以采用无栈异常(stackless exception)?

  • fillInStackTrace 的分配与栈遍历成本
  • 无栈异常的适用场景
  • 性能与诊断的权衡

fillInStackTrace() 会遍历当前调用栈,为每个栈帧创建 StackTraceElement 对象并填入数组,涉及大量对象分配与栈帧遍历,是异常创建的主要开销来源。在异常频率很高的场景(每请求多次、每循环多次)会显著影响 CPU 与 GC。可采用无栈异常:重写 fillInStackTrace() 返回 this(不填充栈),或使用专门构造禁用栈(如 JVM 的 StackTraceElement 不填充),这样异常对象小、创建快,但失去栈信息,只能靠消息/错误码定位。适用场景:异常用作控制流信号(如模拟返回值)、异常被吞掉但用错误码记录、以及高频校验路径;不适合:需要精确堆栈定位的故障诊断。工程上可用"错误码 + 日志关键点"替代完整栈。

栈快照成本与栈深度成正比,本质是"诊断价值"与"性能"的权衡。无栈异常把成本省下来,代价是丢失定位信息,适合可预测、低频次诊断的场景。

public class FastException extends RuntimeException {
    public FastException(String msg) { super(msg); }
    @Override public synchronized Throwable fillInStackTrace() {
        return this;   // 禁用栈填充,减少分配
    }
}
#
★★★

17. == 与 equals 在基本类型、引用类型与包装类上的语义差异

== 与 equals 在基本类型、引用类型与包装类上的语义有何差异?

  • == 的语义(值比较 vs 引用比较)
  • equals 的语义(值比较,需重写)
  • 包装类 == 的缓存陷阱

基本类型上 == 比较的是数值(值相等);引用类型上 == 比较的是引用(是否同一对象,即地址);equals 是 Object 的方法,默认实现与 == 相同(引用比较),需要类重写后才是值比较(如 String、Integer、包装类)。因此在包装类上 == 比较的是引用,其是否相等取决于缓存(-128~127 内复用对象)或是否同一实例,可能与值相等不一致;应使用 equals 比较包装类值。基本类型 == 永远是值比较,不可能用 equals(基本类型无方法)。关键陷阱是:基本类型与包装类混用时,== 触发自动拆箱后按值比较(如 int == Integer 会拆箱比较数值),但两个包装类之间 == 是引用比较。

== 是语言级运算符,语义由操作数类型决定:基本类型比值、引用类型比引用;equals 是方法,默认引用比较、重写后值比较。包装类混用触发拆箱与缓存,是 == 陷阱的根源。

int a = 10; int b = 10;
System.out.println(a == b);        // true 值比较
Integer x = 10, y = 10;
System.out.println(x == y);        // true 缓存
Integer p = 200, q = 200;
System.out.println(p == q);        // false 引用比较
System.out.println(p.equals(q));   // true 值比较
String s1 = "ab", s2 = "ab";
System.out.println(s1 == s2);      // true 常量池复用
System.out.println(new String("ab") == s1); // false
#
★★★

18. @SneakyThrows(Lombok)与隐性异常的代价

Lombok 的 @SneakyThrows 注解是如何实现的?它带来哪些隐性异常的代价?

  • @SneakyThrows 的字节码原理(Unsafe 抛异常)
  • 受检异常被"隐藏"的语义风险
  • 调用方无法感知受检异常的成本

@SneakyThrows 通过 Lombok 在编译期把方法体内抛出的受检异常用"泛型欺骗"技巧重新抛出——它在字节码里把异常类型擦除,实际运行时用 Unsafe 或 JVM 的隐藏机制抛出原始受检异常,从而绕过编译器的 checked exception 检查,使方法签名无需声明 throws。它的代价是"隐性异常":调用方无法从方法签名看到该异常,也不会被强制处理,导致异常被静默传播甚至被上层误吞;受检异常的设计初衷(强制调用方处理)被绕过,破坏了可读性与契约。适用于:对不需要调用方处理的边界类型转换、或与不声明受检异常的第三方库(如某些反射、Lambda)协作。但并不推荐作为常规手段,因为它掩盖了方法可能抛出的异常契约。

@SneakyThrows 是通过编译期字节码改写的"作弊",让受检异常看似非受检。代价是失去编译期契约保障,异常处理责任被隐式转移,滥用会降低代码可维护性。

@SneakyThrows
public void run() {
    Thread.sleep(1000);   // 本应声明 InterruptedException,被 @SneakyThrows 隐藏
}
// 调用方看不到受检异常,sleep 的 InterruptedException 会被静默传播
#
★★★

19. AssertionError 的使用场景与生产开关

AssertionError 的使用场景是什么?生产环境如何开关断言?

  • assert 关键字与 AssertionError
  • -ea/-da 开关
  • 断言 vs 参数校验的区别

assert 关键字在条件为 false 时抛出 AssertionError,用于断言"本应成立的不变量"(如内部不变量、算法中途条件、测试断言),用于开发与测试阶段发现逻辑错误。生产环境默认关闭断言(-ea 开启,-da 关闭,java 默认 -da),因此断言不会在生产执行,也不该用它做参数校验或业务校验(那些必须始终生效,应使用 Objects.requireNonNull 或显式抛 IllegalArgumentException)。AssertionError 是 Error 而非 Exception,语义上表示"程序出现不可预期的内部错误"。生产中若需保留断言,可显式开启 -ea,但通常依赖测试与日志。

断言是"调试期不变量",生产默认关闭;参数校验是"运行时契约",必须始终生效。二者用途不同,断言不能替代参数校验。

int x = compute();
assert x >= 0 : "x must be non-negative: " + x;   // 仅 -ea 时生效
// 参数校验(始终生效):
if (amount < 0) throw new IllegalArgumentException("amount < 0");
#
★★★

20. Checked Exception 与 Unchecked Exception 的设计争议

Checked Exception 与 Unchecked Exception 的设计争议是什么?各自的优缺点如何?

  • 受检异常与未受检异常的语义
  • 强制处理的优点与过度使用的缺点
  • 现代工程实践倾向

Checked Exception(受检,如 IOException)要求调用方必须处理或声明 throws,编译器强制,意图是让可恢复的异常被显式处理;Unchecked Exception(RuntimeException 子类)不强制处理,编译期不检查。设计争议:受检异常保证"编译期契约"、防止遗漏,但常导致过度包装(catch 后又 throw)、渗透到接口签名、与 Lambda/Stream 不兼容(Stream 里无法直接抛受检异常),且多数调用方无法真正恢复,只能向上传播。现代工程实践普遍倾向:对"调用方无能力恢复"的异常(框架、底层系统错误)用不可受检(RuntimeException),对"调用方可通过处理恢复"的才用受检;很多框架(Spring 把大部分异常转为 RuntimeException)经验表明,受检异常在实践中往往带来样板代码而非真实价值。平衡的关键是判断"调用方能否合理恢复"。

争议核心是"编译器强制处理"是否利大于弊。受检异常保证契约但引入样板与传播负担;现代实践倾向对可恢复、调用方有明确处理策略的场景用受检,否则用非受检。

// 受检异常示例(调用方必须处理)
try { Files.readAllBytes(path); } catch (IOException e) { log(e); }
// Stream 中无法直接抛受检异常,需包装成 RuntimeException
lines.stream().map(l -> {
    try { return parse(l); } catch (ParseException e) { throw new RuntimeException(e); }
});
#
★★★

21. Error 与 OutOfMemoryError 的处理策略(不可恢复边界)

Error 与 OutOfMemoryError 的处理策略是什么?什么是不可恢复边界?

  • Error 与 Exception 的层次差异
  • OOM 的不可恢复性
  • 快速失败与兜底策略

Error(如 OutOfMemoryError、StackOverflowError、NoClassDefFoundError)表示 JVM 或系统层面的严重问题,通常不是程序可恢复的。OOM 是"堆无法满足分配"的致命状态,此时继续执行可能再次 OOM 或挂死,属于不可恢复边界。处理策略:不依赖 catch 捕获后恢复,而是保留现场(堆转储、GC 日志、core dump)后快速失败退出(如 -XX:+ExitOnOutOfMemoryError),由容器/编排层重启实例;对可能触发 OOM 的路径(大缓存、无界集合)应通过容量限制、软引用、监控告警预防。栈溢出(StackOverflowError)同样不可恢复。全新视角:Error 也代表"程序不应捕获"的边界,但测试或诊断框架可能捕获它以记录信息后退出。

不可恢复边界意味着"恢复正常状态的努力会失败",所以策略是"诊断 + 快速失败 + 自动化恢复",而不是"优雅恢复"。预防重于捕获。

// JVM 参数:OOM 时转储并退出
// -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data
// -XX:+ExitOnOutOfMemoryError
// 预防:限制缓存大小
Map<String, byte[]> cache = new ConcurrentHashMap<>();
if (cache.size() > MAX) cache.clear();  // 或用软引用/容量上限
#
★★★

22. Helpful NPE 在链式调用(a.getB().getC())中的空值定位精度与性能开销

Helpful NPE(Helpful NullPointerException)在链式调用 a.getB().getC() 中如何定位空值?性能开销如何?

  • JEP 358 Helpful NPE 的异常消息
  • 链式调用中的空值定位精度
  • 性能开销与默认关闭

JEP 358(Helpful NPE)在 JDK 14 引入时默认关闭(需 -XX:+ShowCodeDetailsInExceptionMessages 开启),JDK 15 起默认开启。当 NPE 发生时 JVM 会分析字节码,在异常消息中精确定位是链式调用中哪个环节为 null,例如"a.getB()"或"a.getB().getC()"中究竟是 a 为 null 还是 getB() 返回 null。实现上,JVM 在 NPE 抛出点的字节码中查找被 null 调用的目标,生成精确的"cannot invoke ... because ... is null"消息。这会带来一定开销:JVM 需在异常路径上做字节码分析(主要是异常路径,正常路径零开销),且在某些 JIT 优化下可能被关闭。因此一般认为 Helpful NPE 的定位精度大幅提升,性能开销主要在抛异常路径上、可接受,且可通过 -XX:-ShowCodeDetailsInExceptionMessages 关闭。它不改变 NPE 是可捕获异常的语义,只是改进诊断信息。

Helpful NPE 的价值是"定位精度",把"第几行 NPE"细化为"链上哪个环节为 null",显著提升排错效率;代价是异常路径的字节码分析开销,正常路径无影响。

Class A { B getB() { return null; } }
class B { String getC() { return "c"; } }
A a = new A();
a.getB().getC();   // NPE 消息:Cannot invoke "B.getC()" because "a.getB()" is null
// 精确指出 getB() 返回 null,而非只说第 3 行
#
★★★

23. Java 14 Helpful NullPointerException(JEP 358)的空值链定位信息与 -XX:+ShowCodeDetailsInExceptionMessages

Java 14 的 Helpful NullPointerException(JEP 358)如何提供空值链定位信息?-XX:+ShowCodeDetailsInExceptionMessages 的作用是什么?

  • JEP 358 的机制
  • 空值链定位信息的格式
  • ShowCodeDetailsInExceptionMessages 开关

JEP 358(Helpful NullPointerException)在 JDK 14 引入,当时默认关闭(需 -XX:+ShowCodeDetailsInExceptionMessages 开启);JDK 15 起默认开启。当 NPE 发生时,JVM 从抛出点的字节码中分析出被 null 引用的表达式,在异常消息中生成"cannot invoke X because Y is null"的精确描述,指出链式调用里具体是哪个环节返回 null。该特性由 -XX:+ShowCodeDetailsInExceptionMessages 控制,可通过 -XX:-ShowCodeDetailsInExceptionMessages 关闭,JDK 15 起通过方法级分析能给出更精确的消息。关闭时机:一是追求最小异常路径开销的高性能场景,二是某些混淆/字节码工具导致分析不准确时。它不影响 NPE 的抛出与捕获语义,仅增强诊断信息。

JEP 358 的核心是把"行号级 NPE"升级为"表达式级 NPE 定位",利用字节码分析识别链上的 null 环节;开关控制是可选的性能与兼容性杠杆。

// 触发 NPE 的链式调用
map.get("key").toString();
// 消息示例:Cannot invoke "Object.toString()" because "map.get("key")" is null
// 关闭:java -XX:-ShowCodeDetailsInExceptionMessages App
#
★★★

24. Java 17 密封接口与 JDK 21 模式匹配的穷尽性检查

Java 17 的密封接口(sealed interface)与 JDK 21 的模式匹配如何实现穷尽性检查(exhaustiveness)?

  • sealed 接口与 permits 子类
  • instanceof 模式匹配与 switch 表达式
  • 编译期穷尽性检查

sealed 接口用 permits 显式声明允许的子类型,使子类型集合在编译期可枚举、有限且封闭。JDK 21(record pattern + pattern matching for switch 终版)使 switch 表达式/switch 语句可对 sealed 类型做穷尽性检查:当 switch 覆盖了密封类型的所有 permitted 子类型(如每个 record 子类型都有 case 分支),编译器认为 switch 是穷尽的,无需 default;若漏掉某个子类型,编译器会报错("the switch statement does not cover all possible input values")。这保证编译期完整性——新增子类型时若忘记更新 switch,编译失败,从而避免运行时 MatcherFormatException。穷尽性检查结合模式变量与 record 解构,让 sealed 类型上的分支成为类型安全的代数数据类型风格。

穷尽性检查把"运行时未知分支"变成"编译期错误",配合 sealed 的封闭子类型集,让编译器保证所有可能情况都被处理,是模式匹配的正确性基石。

sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
record Triangle(double b, double h) implements Shape {}

double area(Shape s) {
    return switch (s) {   // 穷尽所有 permits,无需 default
        case Circle c -> Math.PI * c.r() * c.r();
        case Square sq -> sq.s() * sq.s();
        case Triangle t -> 0.5 * t.b() * t.h();
    };
}
#
★★★

25. Java 7+ 多 catch(multi-catch)与类型推断

Java 7+ 的多 catch(multi-catch)如何工作?其类型推断有何限制?

  • multi-catch 语法(catch (A | B e))
  • multi-catch 变量的共同父类型
  • 与序列化/异常类型的关系

Java 7 引入 multi-catch,允许一个 catch 块捕获多个不相关的异常类型:catch (IOException | SQLException e)。这意味着多个异常用同一处理逻辑。类型推断:multi-catch 变量的静态类型是这些异常类型的共同父类型(最精共同父类),因此只能调用共同父类上的方法;若共同父类型是某个异常的超类,则不能直接调用子类特有方法。限制:multi-catch 的类型不能有继承关系(不能说 catch (IOException | FileNotFoundException e),因为后者是前者子类),否则编译错误;且 multi-catch 变量被视为隐式 final(不能重新赋值)。此外在某些泛型场景下共同父类型推断会有边界。multi-catch 消除了重复的 catch 块样板。

multi-catch 是语法糖,把多个 catch 合并;变量类型是最精共同父类,限制了可调用方法,且类型间不能有继承关系。它让"同类处理"更简洁。

try {
    read();
} catch (IOException | SQLException e) {   // multi-catch
    log(e);   // e 的静态类型是这两个异常的共同父类 Exception
}
// 编译错误:catch (IOException | FileNotFoundException e) 互有继承
#
★★★

26. Java 8 接口默认方法的菱形继承与冲突解决

Java 8 接口默认方法(default method)在菱形继承场景下如何解决冲突?

  • 默认方法的菱形继承
  • 冲突解决规则(类优先、最具体接口优先)
  • 显式覆盖 super 调用

Java 8 默认方法允许接口有实现,但引入菱形继承冲突:一个类实现多个接口,若这些接口有同名默认方法,需要冲突解决。规则依次为:1)类中的方法(显式实现)优先于接口默认方法;2)若类未实现,则选择"最具体"的接口默认方法(继承树上最贴近类的那个);3)若仍无法区分(两个无关接口都提供实现),则必须显式覆盖并指定调用哪个接口的 super 方法(interfaceName.super.method())。类优先意味着实现优先。若真的冲突,编译器强制显式解决,避免歧义。类优先于接口、父类方法优先于接口默认方法。

冲突解决的核心是"最具体优先 + 类优先";无法确定时编译器要求显式用 X.super.m() 指定。这保证了菱形继承下默认方法不会产生歧义。

interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }
class C implements A, B {
    // 冲突:A 与 B 都有默认方法,必须显式解决
    public void hello() { A.super.hello(); }   // 显式指定调用 A 的默认实现
}
#
★★★

27. Java 8/11/17/21/25 各版本新增的语言特性对日常编码的影响

Java 8/11/17/21/25 各版本新增的语言特性对日常编码有什么影响?

  • Java 8:Lambda/Stream/Optional/默认方法
  • Java 17:sealed、模式匹配、record
  • Java 21:record pattern、虚拟线程、switch 模式匹配

各版本语言特性影响日常编码方式:Java 8 引入 Lambda/Stream/Optional/接口默认方法,改变集合处理与函数式风格;Java 10 引入 var(JEP 286),Java 11 支持 lambda 参数 var(JEP 323)并引入 String.isBlank/strip/lines 等;Java 14 引入 record 与帮助性 NPE,Java 16 定稿 record,Java 17 引入 sealed 类与 instanceof 模式匹配,Java 21 定稿 record pattern、switch 模式匹配、虚拟线程与结构化并发预览,Java 25 定稿灵活构造器体(JEP 513)等,字符串模板(JEP 430/459)在 JDK 21/22 预览后于 JDK 23 起被搁置。这些特性让代码更简洁、类型安全、模式化,减少样板代码并提升可读性;同时要求开发者在版本升级时拥抱新语法(如用 record 定义数据载体、用模式匹配代替 instanceof 强转、用虚拟线程简化并发)。

语言特性是生产力工具,理解"哪个版本引入什么"帮助团队在代码库中采用最合适的现代写法,同时新特性(sealed、模式匹配、record)强化类型安全与编译期检查。

// Java 8: Lambda + Stream
list.stream().filter(x -> x > 0).mapToInt(i -> i).sum();
// Java 16+: record
record Point(int x, int y) {}
// Java 21: switch 模式匹配 + record pattern
String s = switch (obj) {
    case Point p -> "p(" + p.x() + "," + p.y() + ")";
    case String str -> str;
    default -> "other";
};
#
★★★

28. Java 关键字 yield 在 switch 表达式中的作用

Java 关键字 yield 在 switch 表达式中的作用是什么?

  • switch 表达式与 yield 返回值
  • yield 与 return 的区别
  • 箭头语法与块语法

yield 是 Java 14 引入的 switch 表达式的关键字,用于在 switch 表达式的块(block)语法(case X: 后跟用 {} 包裹的块)中返回一个值作为整个 switch 表达式的值。与 return 不同,yield 只结束当前 switch 表达式并产生其结果,不会退出方法;严格来说 yield 是 switch 表达式的一部分,不能用于普通代码块。箭头语法(case X -> value)中每个分支直接是一个表达式,无需 yield;只有当分支需要多条语句(块体)时,才用 yield 返回结果。yield 使 switch 能作为表达式返回确定值,配合穷尽性检查与模式匹配,写出类型安全的分支逻辑。

switch 表达式是"有值的表达式",yield 是其块体分支的返回值渠道;箭头语法用表达式直接返回,块语法用 yield 返回。二者语义一致。

int days = switch (month) {
    case 1, 3, 5, 7, 8, 10, 12 -> 31;          // 箭头语法直接返回
    case 4, 6, 9, 11 -> 30;
    case 2 -> {
        int leap = isLeap(year) ? 29 : 28;      // 块体用 yield 返回
        yield leap;
    }
    default -> throw new IllegalArgumentException("bad month");
};
#
★★★

29. Java 内部类(成员/局部/匿名/静态内部类)的语义差异

Java 内部类的四种类型(成员/局部/匿名/静态内部类)在语义上有何差异?

  • 四种内部类的定义位置与作用域
  • 对外部类的引用(非静态内部类持有外部实例引用)
  • 静态内部类与成员内部类的区别

四种内部类:1)成员内部类(member inner class):定义在类内部、非静态,持有外部类实例的隐式引用,可访问外部类私有成员,必须通过外部实例 new(outer.new Inner()),不能有静态成员;2)局部内部类(local inner class):定义在方法/块内,作用域局限于该块,可捕获方法中的有效 final 局部变量;3)匿名内部类(anonymous inner class):没名字、在表达式处定义,常用于实现接口/抽象类,可捕获有效 final 局部变量;4)静态内部类(static nested class):用 static 修饰,不持有外部实例引用,可独立实例化,属于"嵌套类"而非严格内部类。核心差异是"是否持有外部实例引用"以及"定义位置与作用域"。

非静态内部类(成员/局部/匿名)隐式持有外部实例引用,导致生命周期被外部对象延长、可能内存泄漏;静态内部类无此引用,更安全。工具类(如 Builder)通常用静态内部类。

class Outer {
    private int x;
    class Inner { void m() { System.out.println(x); } }   // 成员内部类,可访问外部私有
    static class Nested { static void m() {} }            // 静态嵌套类,无外部引用
    void method() {
        int v = 10;
        class Local { Local() { System.out.println(v); } } // 局部内部类,捕获有效 final
        Runnable r = new Runnable() { public void run() { System.out.println(v); } }; // 匿名
    }
}
// 成员内部类实例化:new Outer().new Inner()
#
★★★

30. Java 协变返回类型(Covariant Return Type)

什么是 Java 的协变返回类型(Covariant Return Type)?

  • 覆写方法的返回类型放宽
  • 协变返回类型与桥接方法
  • 与泛型协变的关系

协变返回类型指覆写(override)父类方法时,子类方法的返回类型可以是父类返回类型的子类型(更具体的类型),而不仅是完全相同。Java 5 起支持。例如父类方法返回 Object,子类覆写返回 String。这提高了 API 的类型精度,调用方通过子类引用时无需强转。实现上,编译器会生成桥接方法(bridge method)使字节码层面保持与父类签名一致(返回 Object),同时指导调用到实际子类方法。协变返回类型建立在"返回类型可以是自类型"的规则上(Java 5 泛型加入后推广),与泛型协变(如 List<? extends Number>)不同,这是继承层面的协变。

协变返回类型让子类覆写能收窄返回类型,提升类型安全与可用性,编译器用桥接方法兼容字节码签名。

class Animal { Animal reproduce() { return new Animal(); } }
class Dog extends Animal {
    @Override Dog reproduce() { return new Dog(); }   // 协变返回类型:返回 Dog <: Animal
}
Dog d = new Dog().reproduce();   // 无需强转,直接得到 Dog
#
★★

31. Java 异常的层次结构(Throwable/Error/Exception/RuntimeException)

请描述 Java 异常的层次结构:Throwable、Error、Exception、RuntimeException 的关系?

  • Throwable 根类
  • Error 与 Exception 的分支
  • Checked 与 Unchecked 异常

Throwable 是 Java 所有可抛出错误的根类,直接子类为 Error 和 Exception。Error 表示 JVM/系统级别的严重问题(OutOfMemoryError、StackOverflowError、NoClassDefFoundError),通常不可恢复,不应捕获处理。Exception 又分为两大类:受检异常(Checked Exception,编译期必须处理,如 IOException、SQLException,是 Exception 但非 RuntimeException 的子类)与未受检异常(Unchecked Exception,即 RuntimeException 及其子类,如 NullPointerException、IllegalArgumentException、ClassCastException,编译期不强制处理)。流为:受检异常必须声明或捕获;RuntimeException 与 Error 都是未受检(编译期不检查)。层次结构决定了异常的处理职责与编译期约束。

层次结构是"可抛出物"的分类:Error=系统级不可恢复,Exception=程序级可处理,RuntimeException=未受检,其余 Exception=受检。这决定了编译期强制处理与捕获策略。

// Throwable
//   ├─ Error (不可恢复,不捕获)
//   │    ├─ OutOfMemoryError, StackOverflowError, NoClassDefFoundError
//   └─ Exception
//        ├─ RuntimeException (未受检): NPE, IllegalArgumentException, ClassCastException
//        └─ 其他受检异常: IOException, SQLException (必须处理)
#
★★

32. Java 数值类型的提升规则与溢出陷阱

Java 数值类型的提升规则是什么?有哪些溢出陷阱?

  • 二元运算符的数值提升
  • 整数溢出与回绕
  • long 与 int 的混合运算

Java 数值提升规则:在二元算术运算中,若操作数类型不同,会按"数值提升"统一:byte/short/char 先提升为 int(除非有更大类型),然后若有一个是 long 则全部提升为 long,float 则全为 float,double 则全为 double。因此 byte+byte 结果至少是 int,byte 与 int 混合结果是 int。溢出陷阱:int 运算溢出时回绕(wrap around),结果可能为负或错误(如 Integer.MAX_VALUE+1 == Integer.MIN_VALUE),且不抛异常;字面量运算若超出 int 范围需加 L 后缀;混合运算时若操作数是 int 而另一个是 long,需注意结果精度。避免溢出:用 long 或 long 运算,或使用 Math.addExact 等精确方法(溢出抛 ArithmeticException),或使用 BigInteger/BigDecimal。

数值提升决定运算结果的类型,溢出是"静默回绕",容易产出错误结果而不报错。理解提升规则并保持类型一致(尤其用 long 运算、Math.*Exact、BigInteger)可避免隐患。

byte b = 100;
int r = b + b;                 // byte+byte 提升为 int
int x = Integer.MAX_VALUE;
int y = x + 1;                 // 溢出回绕为 Integer.MIN_VALUE,不抛异常
long z = x + 1;                // 仍是 int 运算溢出后赋给 long,仍是 -2147483648
long w = (long) x + 1;         // 先提升 long,结果为 2147483648
int safe = Math.addExact(x, 1); // 溢出抛 ArithmeticException
#
★★

33. Java 继承与组合的取舍,里氏替换原则的实际违反场景

Java 中继承与组合如何取舍?里氏替换原则(LSP)有哪些实际违反场景?

  • 继承与组合的取舍(is-a vs has-a)
  • 里氏替换原则
  • 违反 LSP 的典型场景

继承表示 is-a 关系,适合"子类是父类的特化"且子类可替换父类(LSP);组合表示 has-a 关系,用包含对象实现复用,更灵活、松耦合,避免破坏封装。取舍:当仅需复用代码而非真的 is-a 时用组合;继承层级过深、子类覆写破坏父类行为时用组合。LSP 要求:子类必须能替换父类且不改变行为契约。实际违反场景:1)子类覆写方法抛出不适用于父类语义的异常(如父类不抛,子类抛受检异常);2)子类收窄输入(如父类接受任意值,子类只接受部分值,如移除元素时抛 UnsupportedOperationException);3)子类改变不变量(如 Stack 是 Vector 的子类,但破坏了不变量);4)父子类对相等性/排序语义不一致。违反 LSP 导致用父类类型引用时出现意外行为。

继承复用代码但一旦违反 LSP 就破坏多态正确性;组合通过接口+委托实现复用,规避继承陷阱。判断"是否 is-a"且"能否安全替换"决定用继承还是组合。

// 违反 LSP:子类覆写破坏父类契约
class Base { void process(int x) { if (x < 0) throw new IllegalArgumentException(); } }
class Bad extends Base {
    @Override void process(int x) { if (x < 0) return; }  // 收窄输入,静默忽略非法值
}
// 正确做法:优先组合
class Service { private final Base base = new Base(); void run(int x){ base.process(x); } }
#
★★

34. Lambda 中的 checked exception 包装策略

Lambda 中如何处理 checked exception(受检异常)?常用的包装策略是什么?

  • 函数式接口一般不声明受检异常
  • 受检异常在 Lambda 中的限制
  • 包装策略(包装成 RuntimeException)

标准函数式接口(Function、Consumer、Runnable 等)的抽象方法不声明受检异常,因此 Lambda 体不能直接抛出受检异常(会编译失败)。处理策略:1)包装成 RuntimeException(如 new RuntimeException(e)),缺点是丢失类型、调用方只能捕获 RuntimeException;2)使用自定义的、能抛出受检异常的函数式接口(如 ThrowingFunction { R apply(T) throws Exception; }),配合工具方法在调用处解包;3)用 SneakyThrows 风格绕过;4)在 Lambda 内 try/catch 就地处理。工程上推荐:明确区分"可恢复"与"不可恢复",I/O 等受检异常通常包装成携带 cause 的领域异常,或在函数式接口边界用 Throwing 变体并统一解包。

受检异常与函数式接口的签名机制冲突,因为函数式接口抽象方法无法声明泛化的 throws。包装策略的核心是"哪里捕获、如何保留 cause 与类型"。

// 自定义可抛受检异常的函数式接口
@FunctionalInterface
interface ThrowingConsumer<T> { void accept(T t) throws Exception; }
static <T> Consumer<T> uncheck(ThrowingConsumer<T> c) {
    return t -> { try { c.accept(t); } catch (Exception e) { throw new RuntimeException(e); } };
}
list.forEach(uncheck(this::writeFile));   // writeFile 抛 IOException
#
★★

35. _(Underscore)在 Java 8/9 前后的语义变化

_(下划线)在 Java 8/9 前后的语义有何变化?

  • Java 8 中 _ 作为合法标识符
  • Java 9 起 _ 成为关键字/非法标识符
  • 迁移影响

在 Java 8 及以前,_ 是合法的标识符,可作为变量名(如 int _ = 5)。Java 9 起 _ 被保留为关键字,不能用作标识符(变量名、参数名、类名等,包括 lambda 参数),编译会报错。语义变化是为了避免误用并给未来语法(如未命名变量)留空间。受影响:Java 8 时代用 _ 作变量名的代码在 Java 9+ 编译失败,需重命名;Java 8 中 _ 作为 lambda 参数仅产生警告,Java 9 起同样报错。Java 22(JEP 456 未命名变量与模式)起,_ 重新允许作为 lambda 忽略参数(如 (a, _))。迁移时需要把 _ 变量重命名为有意义的名称。

_ 从"合法标识符"变为"关键字/非法标识符",是 Java 版本语义的变化,影响旧代码迁移;新代码不应使用 _ 作变量名。

// Java 8:int _ = 5; 合法
// Java 9+:int _ = 5; 编译错误:'_' is a keyword
// Java 9+ 中 _ 只能作为 lambda 忽略参数(部分版本)
list.forEach((a, _) -> System.out.println(a));  // 依版本而定
#
★★

36. clone() 的浅拷贝、深拷贝与替代方案(拷贝构造器/工厂)

clone() 的浅拷贝与深拷贝如何实现?有哪些替代方案(拷贝构造器/工厂)?

  • Object.clone() 与 Cloneable 标记接口
  • 浅拷贝 vs 深拷贝
  • 替代方案:拷贝构造器、工厂方法、序列化

clone() 是 Object 的 protected 方法,需实现 Cloneable 标记接口并重写为 public 才能调用,否则抛 CloneNotSupportedException。默认 clone() 是浅拷贝:复制对象字段引用,但引用指向的对象是共享的(不复制引用对象本身)。深拷贝需手动递归复制所有引用对象(或通过序列化/复制构造器)。clone() 的缺陷:不调用构造器、语义隐晦、需处理 Cloneable 与异常、深拷贝容易漏,因此不推荐。替代方案:1)拷贝构造器(new Person(p)),清晰、可控、可做深拷贝;2)工厂方法(Person.copyOf(p));3)record 的副本构造(record 只读,天然不可变);4)序列化深拷贝(有开销与安全风险)。现代实践倾向用拷贝构造器/工厂而非 clone。

clone() 是"古老而脆弱"的机制,浅拷贝是默认、深拷贝需手动实现;拷贝构造器/工厂更清晰可控,且能保证不可变性与防御性拷贝。

class Person implements Cloneable {
    List<String> tags;
    @Override public Person clone() {                 // 浅拷贝
        try { return (Person) super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); }
    }
    Person deepCopy() { Person p = new Person(); p.tags = new ArrayList<>(tags); return p; } // 深拷贝
    Person(Person src) { this.tags = new ArrayList<>(src.tags); }  // 拷贝构造器
}
#
★★

37. enum 的进阶用法(抽象方法、EnumSet/EnumMap、valueOf 边界)

enum 的进阶用法有哪些?包括抽象方法、EnumSet/EnumMap、valueOf 的边界?

  • enum 的抽象方法(每个常量实现)
  • EnumSet/EnumMap 的高效位图实现
  • Enum.valueOf 与 name/ordinal

enum 是特殊的类,可定义字段、构造器、方法,甚至抽象方法(每个常量实现不同逻辑)。EnumSet 与 EnumMap 是基于位图/数组的高效集合:EnumSet 用 long 位集表示,EnumMap 用数组按 ordinal 索引,均非常高效且内存紧凑。valueOf(Class, String) 按名称查找常量,若名称不存在抛 IllegalArgumentException(而 name() 返回常量名,ordinal() 返回定义顺序)。边界:valueOf 使用名称(区分大小写),不是 ordinal;重复定义名称不可行;不要依赖 ordinal 做持久化(顺序变化会破坏数据)。进阶用法:在常量上实现抽象方法(如操作符分派)、用 EnumMap 做状态机、用 EnumSet 做权限位集。

enum 的进阶价值在于"常量携带行为"(抽象方法)与"高效集合"(EnumMap/EnumSet)。valueOf 按名称查找,ordinal 是数组索引基础,持久化应存名称而非 ordinal。

enum Op {
    ADD { int apply(int a, int b) { return a + b; } },
    MUL { int apply(int a, int b) { return a * b; } };
    abstract int apply(int a, int b);
}
Op op = Enum.valueOf(Op.class, "ADD");   // 按名称,不存在抛 IllegalArgumentException
EnumSet<Op> set = EnumSet.of(Op.ADD, Op.MUL);   // 位图实现
EnumMap<Op, String> map = new EnumMap<>(Op.class); // 数组实现
#
★★

38. final 字段的安全发布(Safe Publication)保证

final 字段如何提供安全发布(Safe Publication)保证?

  • final 字段的 JMM 语义
  • 安全发布与内存可见性
  • 构造器与 final 的保证

在 JMM(Java 内存模型)中,final 字段有特殊的发布保证:当一个对象的引用被正确发布(通过 synchronized、volatile、final、或正确构造后的引用传递)后,其他线程看到的 final 字段值一定是构造器写入的最终值,且不会因为重排序而看到中间/默认值。具体地,final 字段的写发生在构造器结束前,且读 final 字段的线程不会看到构造器中的重排序(final 字段禁止某些重排序,保证构造器内 final 字段的写与对象引用发布之间的顺序)。代价是:final 字段的读在某些时候需要额外的内存屏障(例如在构造器内 final 字段的写后,存在 store 屏障),但通常比 volatile 便宜。安全发布需要"引用本身正确发布"(final 并不能单独保证引用发布安全,除非引用本身是 final 或通过其他安全机制)。

final 字段保证"构造完成后的值对其他线程可见且按顺序",是线程安全不可变对象的基础。但引用本身仍需安全发布(如 final 引用、或其他同步机制)。

public final class Immutable {
    private final int x;
    private final String s;
    public Immutable(int x, String s) { this.x = x; this.s = s; }
}
// 正确发布后,其他线程读取 x/s 一定看到构造器写入值,不受重排序影响
Immutable ref = new Immutable(1, "a");   // 引用本身需安全发布
#
★★

39. instanceof 模式匹配(JEP 394/420)与 switch 模式匹配的演进

instanceof 模式匹配(JEP 394/420)与 switch 模式匹配如何演进?

  • instanceof 模式匹配(JEP 394)
  • switch 模式匹配(JEP 420/441)
  • 模式变量与作用域

instanceof 模式匹配(JEP 394 在 Java 16 定稿)允许在 instanceof 后带模式变量,避免显式强转:"if (o instanceof String s) { ... }",匹配成功后 s 在作用域内可用,且无需再 cast。switch 模式匹配(JEP 420 预览,Java 21 JEP 441 定稿)把模式匹配扩展到 switch,支持类型模式、null 处理、record 解构模式,并配合穷尽性检查。演进路径:instanceof 模式匹配(16)→ switch 模式匹配(17 预览/21 定稿)→ record pattern(19 预览/21 定稿)→ 模式匹配的泛化。这让"鸭子类型判断"变成类型安全的代数分支,减少 cast 与样板代码。

模式匹配把"类型检查+强转"合并为"匹配+解构",switch 模式匹配进一步支持组合分支与穷尽性,是 Java 语言向更表达式化、类型安全演进的关键。

// instanceof 模式匹配(16)
if (obj instanceof String s) { System.out.println(s.length()); }
// switch 模式匹配(21)
String r = switch (obj) {
    case Integer i -> "int " + i;
    case String s -> "str " + s;
    case int[] arr -> "arr " + arr.length;
    default -> "other";
};
#
★★

40. null 对象模式与 Optional 的取舍

null 对象模式(Null Object Pattern)与 Optional 如何取舍?

  • null 对象模式(空实现对象)
  • Optional 的容器语义
  • 适用场景与取舍

null 对象模式是提供一个"无操作"的实现对象(如 NoOpLogger、NullLogger),替代 null 引用,使调用方无需判空即可调用方法;Optional 是容器,显式表示"可能为空的值",用 map/flatMap/orElse 等链式处理空值。取舍:null 对象模式适合"对象实现了接口、空对象有默认行为"(如实体的空实现、空缓存),让方法可被安全调用;Optional 适合"返回值可为空、需要显式处理"的函数式链式场景。Optional 不适合作为字段/参数(会造成每次都判空),适合作为返回值;null 对象模式适合协同对象(Logger、Sink)。二者不是二选一,可结合:用 Optional 表达"可能无值",用 null 对象表达"有默认行为"。关键:避免直接用 null 在不同层间传递,明确"空"的语义。

null 对象模式把"无"变成"有默认行为的对象",Optional 把"无"变成"显式容器"。选择取决于"空"是"无操作对象"还是"待处理的值"。

// null 对象模式
interface Logger { void log(String m); }
class NullLogger implements Logger { public void log(String m) {} } // 空实现
Logger logger = config.getLogger();  // 若无则返回 NullLogger,调用安全
// Optional(返回值)
Optional<String> getName() { return maybeName; }
String n = getName().orElse("unknown");
#
★★

41. 参数校验的快速失败,Objects.requireNonNull/requireNonNullElse 与参数对象自校验(Bean Validation)在异常类型选择上的边界

参数校验的快速失败中,Objects.requireNonNull/requireNonNullElse 与 Bean Validation 在异常类型选择上有何边界?

  • Objects.requireNonNull 抛 NPE
  • Bean Validation 抛 ConstraintViolationException
  • 参数校验异常类型的选择

方法参数校验的快速失败(fail-fast)应尽早抛异常。Objects.requireNonNull(value) 在 null 时抛 NullPointerException(可带消息),requireNonNullElse 提供默认值;Bean Validation(JSR 380)用注解(@NotNull、@NotBlank、@Size)声明式校验,不满足时抛 MethodArgumentNotValidException/ConstraintViolationException。异常类型选择的边界:null 参数校验用 NPE(Objects.requireNonNull)——语义明确"参数为空";业务/格式约束(非空字符串、长度、范围)用 Bean Validation 或 IllegalArgumentException(语义"参数不合法")。实践上:简单的 null 检查用 Objects.requireNonNull 快速失败;复杂约束用 Bean Validation 在入口统一校验;自定义业务规则抛具体业务异常。边界在于区分"null"与"非法值",避免把所有校验都混成 NPE 或全部使用 God-style 校验。

NPE 表达"缺乏必需参数",IllegalArgumentException/ConstraintViolation 表达"参数不合法"。按语义选择异常类型,让调用方能准确理解失败原因并采取对应处理。

public void process(String name, int age) {
    Objects.requireNonNull(name, "name must not be null");   // null -> NPE
    if (age < 0) throw new IllegalArgumentException("age must be >= 0"); // 非法值
}
// Bean Validation 注解式校验
// @NotNull @Size(min=1,max=50) String name;  // 不满足抛 ConstraintViolationException
#
★★

42. package-info.java 与包级注解的应用

package-info.java 的作用是什么?包级注解如何应用?

  • package-info.java 的用途
  • 包级注解定义
  • 包文档与 Javadoc

package-info.java 是一个特殊文件,用于声明包级信息:包级注解(annotations on package)、包级 Javadoc 文档、以及包名声明。它没有类,只有 package 声明,可放置包级注解(如 @NonNullApi、@ParametersAreNonnullByDefault、@XmlSchema 等)和包文档注释。包级注解通过 ElementType.PACKAGE 定义,作用于整个包。应用场景:为包声明默认约束(如整个包参数非空)、为序列化/XML 定义包级配置、包级 Javadoc 说明包的用途。编译时 JAVA 会为每个包生成 package-info 类文件。

package-info.java 是"包级元数据"的载体,让注解与文档作用域覆盖整个包而不是单个类,是库与框架常用配置手段。

// package-info.java
@NonNullApi
package com.example.api;
import org.springframework.lang.NonNullApi;  // 该包所有方法参数默认非空
#
★★

43. super 在泛型与构造中的访问限制

super 在泛型与构造中的访问限制是什么?

  • super 调用父类构造器
  • super 访问父类成员
  • super 与泛型无关

super 在构造中用于调用父类构造器(super(...)),且必须是构造器第一条语句;语法上 super 也可访问父类成员(super.field、super.method())。限制:super(...) 必须出现在构造器第一行,因此不能与 this(...) 同时出现(this 也必须是第一行);不能访问父类私有成员;super 不能用于静态上下文。需注意:super 与泛型无关——泛型通配符中的 ? super T 是"逆变下界",与关键字 super 是两回事,不能混淆。super 关键字用于继承访问,super 通配符用于泛型类型边界。

super 关键字是"父类访问"的语法,super 通配符是"泛型下界"的语义,二者同名但截然不同。构造器第一行限制是反初始化顺序的保证。

class Base { Base(int x) {} }
class Child extends Base {
    Child(int x) { super(x); }   // super() 必须第一行
    // 泛型:? super T 是下界通配符,与 super 关键字无关
}
void m(List<? super Integer> list) { list.add(1); }  // 下界,可写
#
★★

44. switch 表达式箭头语法(yield)与穷尽性检查(exhaustive switch)在 sealed 接口上的应用

switch 表达式箭头语法(yield)与穷尽性检查在 sealed 接口上如何应用?

  • 箭头语法与 yield
  • 穷尽性检查
  • sealed 接口上的 switch

switch 表达式箭头语法(case X -> value)是表达式,每个分支返回一个值;若分支是块体则用 yield 返回。穷尽性检查(exhaustive switch):编译器要求 switch 表达式覆盖所有可能的值类型,否则需 default 分支;对 sealed 接口,编译器能枚举所有 permitted 子类型,若 switch 覆盖了全部子类型则视为穷尽、无需 default,遗漏则编译报错。这保证密封类型的分支完整。应用:在 sealed 接口(如 Shape 的子类型 Circle/Square/Triangle)上用 switch 表达式作为函数,每个 case 一个 record pattern,返回类型一致且穷尽,编译器保证新增子类型时 switch 必须更新。Arrow 语法让 switch 更简洁,配合模式匹配与 sealed 成为类型安全的代数分支。

箭头语法让 switch 成为有值的表达式,sealed 提供的封闭子类型集让编译器能做穷尽性检查,二者结合实现"编译期保证所有分支被处理"。

sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double s) implements Shape {}
double area(Shape s) {
    return switch (s) {              // 穷尽,无需 default
        case Circle c -> Math.PI * c.r() * c.r();
        case Square sq -> sq.s() * sq.s();
    };
}
#
★★

45. try-with-resources 的实现原理与资源关闭顺序

try-with-resources 的实现原理是什么?资源关闭顺序如何?

  • try-with-resources 语法与 AutoCloseable
  • 关闭顺序(逆序)
  • suppressed 异常

try-with-resources 在 try 括号中声明一个或多个 AutoCloseable 资源,编译期自动展开为:try 块结束后自动调用 close(),并处理多个资源。关闭顺序:按声明顺序的逆序关闭(最后声明的先关闭),即"后进先出"。若 try 块正常返回或抛出异常,都会关闭资源;若 try 抛异常且 close() 也抛异常,close 的异常作为 suppressed 附加到主异常(addSuppressed),主异常照常抛出。编译器自动生成 catch/finally 逻辑,无需手动 close。实现上每个资源在 try 后自动 close,多个资源按逆序。若资源实现了 AutoCloseable 且 close 抛异常,try-with-resources 会正确保留主异常并记录 suppressed。

try-with-resources 是"自动关闭 + 异常抑制"的语法糖,关闭顺序逆序保证依赖关系(先关内部资源),suppressed 机制避免关闭异常覆盖主异常。

try (Resource a = new Resource("a");   // 1. 声明
     Resource b = new Resource("b")) {  // 2. 声明
    a.use();
}   // 关闭顺序:b.close() 先,a.close() 后(逆序)
// 若 a.use() 抛 ex,b.close() 抛关闭异常,则 关闭异常 作为 ex 的 suppressed
#
★★

46. var 局部变量类型推断的边界(不能用于字段/方法签名)

var 局部变量类型推断的边界是什么?为什么不能用于字段或方法签名?

  • var 用于局部变量
  • var 不能用于字段/参数/返回类型
  • 类型推断的边界

var(Java 10+)允许局部变量类型推断,即从初始化器推断类型,如 var x = "abc" 推断为 String。边界:var 只能用于局部变量(含 for 循环变量、try-with-resources 资源),不能用于:字段(成员变量)、方法参数、方法返回类型、lambda 参数、捕获类型等。原因:var 依赖"从初始化器推断",字段/参数/返回类型没有初始化器或需要显式契约,若用 var 会破坏类型签名与 API 契约,且无法推断。var 是"局部变量类型推断"而非"动态类型",类型在编译期确定(静态类型),运行时仍是强类型。var 不能用于没有初始化器的地方(如 var x; 报错),也不能用于多态目标(var 推断的是初始化器静态类型)。

var 只减少"局部变量类型书写"的样板,不改变静态类型语义;字段/参数/返回类型是 API 契约,必须显式声明,故 var 不适用。

var name = "hello";        // String(静态类型)
var list = new ArrayList<String>(); // ArrayList<String>
// 非法用法:
// var field;  -> 字段不能 var
// void m(var x) {} -> 参数不能 var
// var x;  -> 必须初始化
#
★★

47. 不可变对象的构造策略与防御性拷贝

不可变对象的构造策略是什么?如何用防御性拷贝保证不可变性?

  • 不可变对象构造要点(final 字段、无 setter)
  • 防御性拷贝
  • 不可变性与线程安全

不可变对象构造策略:1)类声明为 final(防止子类重写破坏不可变);2)所有字段 final 且私有;3)不提供 setter;4)构造器设置所有字段;5)若字段引用可变对象,用防御性拷贝(defensive copy)——构造时复制传入的可变对象(如 new ArrayList<>(src)),访问时返回副本(或不可变视图),避免外部通过引用修改内部状态;6)禁止派生类修改。防御性拷贝保证外部传入或取出的可变对象不会影响内部不可变字段。不可变对象天然线程安全(无状态变化),可安全共享。record 天然的不可变(字段 final、无 setter),但若字段是可变类型,record 只保证浅不可变,仍需注意。

不可变性的关键是"外部无法改变内部状态",防御性拷贝隔离可变引用,是构造不可变对象的核心技巧。record 简化了不可变类型,但需注意非深不可变。

public final class ImmutablePerson {
    private final String name;
    private final List<String> tags;   // 可变引用
    public ImmutablePerson(String name, List<String> tags) {
        this.name = name;
        this.tags = new ArrayList<>(tags);   // 防御性拷贝(构造时复制)
    }
    public List<String> getTags() {
        return new ArrayList<>(tags);        // 防御性拷贝(访问时复制)
    }
}
#
★★

48. 原始类型、原始类型包装与 Number 子类的关系

原始类型、原始类型包装与 Number 子类之间是什么关系?

  • 原始类型与包装类
  • Number 抽象类与包装类层次
  • 自动装箱拆箱

原始类型(int/long/double 等)是值类型,无对象;其包装类(Integer/Long/Double 等)是对象,用于泛型、集合与需要对象的地方。所有数值包装类(Integer、Long、Short、Byte、Float、Double、AtomicInteger 等)都继承自 Number 抽象类,Number 提供了 intValue()、longValue()、doubleValue() 等抽象方法,使不同数值包装类之间可统一转换。关系:原始类型 <-> 包装类通过自动装箱拆箱(boxing/unboxing)转换;包装类通过继承 Number 参与泛型与数值转换。注意:Character、Boolean 不是 Number 子类(非数值);自动装箱时缓存范围影响 == 比较。Number 子类用于把数值包装类统一当作"数值对象"处理。

原始类型是值,包装类是对象(继承 Number 用于数值抽象),自动装箱连接二者。Number 把数值包装类统一为"可转换的数值对象"。

int i = 5;                  // 原始类型
Integer boxed = i;          // 自动装箱 -> Number 子类
Number n = boxed;           // 包装类都是 Number 子类
double d = n.doubleValue(); // Number 统一转换
char c = 'a'; Character ch = c; // Character 不是 Number 子类
#
★★

49. 发生 OutOfMemoryError 后为什么不应依赖常规 catch 恢复,进程退出前可安全执行哪些最小动作

发生 OutOfMemoryError 后为什么不应依赖常规 catch 恢复?进程退出前可安全执行哪些最小动作?

  • OOM 的不可恢复性
  • catch 恢复的危险
  • 退出前的最小动作(日志、转储)

OOM 意味着堆无法满足分配,此时 JVM 已处于"内存耗尽"状态,继续执行任何需要分配对象的代码都可能再次 OOM 或挂死,因此不应依赖常规 catch 恢复——CBD 恢复往往失败,且可能因内存不足导致更严重的不可控状态。进程退出前可安全执行的最小动作:1)把已捕获的堆栈/错误信息写入日志或文件(但避免分配大对象);2)触发堆转储(如 -XX:+HeapDumpOnOutOfMemoryError 自动生成,或用 Unsafe 触发);3)输出关键诊断信息(OOM 类型、线程栈、堆使用);4)显示调用 System.exit 或让 -XX:+ExitOnOutOfMemoryError 自动退出,交由编排层重启。最小动作应尽量"无分配或极少量分配",避免在 OOM 后继续分配。

OOM 后恢复是"在内存耗尽状态下尝试修复",通常徒劳。正确做法是保留诊断现场并快速退出,让外部编排恢复,最小动作以"不加剧内存压力"为原则。

// JVM 参数兜底
// -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data -XX:+ExitOnOutOfMemoryError
// 代码中不 catch OOM 恢复;如需记录,仅做最小无分配动作后退出
// 例如由 ShutdownHook 记录后 System.exit(1)
#
★★

50. 多个 AutoCloseable 资源在业务异常后又关闭失败时,主异常与 suppressed 异常的顺序如何验证

多个 AutoCloseable 资源在业务异常后又关闭失败时,主异常与 suppressed 异常的顺序如何验证?

  • try-with-resources 的 suppressed 机制
  • 主异常与 suppressed 的顺序
  • 验证方法与断言

当 try-with-resources 中 try 块抛出业务异常,且某个资源 close() 也抛出异常时,业务异常作为主异常抛出,close 异常作为 suppressed 异常通过 addSuppressed 附加到主异常上。顺序:主异常是 try 块抛出的异常;多个资源按逆序关闭,若其中多个 close 抛异常,则后关闭的(先声明的)异常在前面,且 close 异常按关闭顺序依次 addSuppressed。验证方法:捕获主异常,调用 getSuppressed() 获取 suppressed 数组,断言其数量与类型与预期一致;用 JUnit 的 assertThrows 捕获主异常后再检查主异常类型与 getSuppressed() 内容。注意:业务异常(try 块)是主异常,而所有关闭异常都被抑制(suppressed),不会覆盖主异常。

验证的关键是"主异常 = try 块业务异常,关闭异常 = suppressed"。用 getSuppressed() 检查顺序与内容,可定位 try-with-resources 的关闭行为是否符合预期。

try (Resource a = new Resource(); Resource b = new Resource()) {
    throw new BizException("main");    // 主异常
} catch (BizException e) {
    e.getSuppressed();   // 含 close 抛出的异常(若有关闭异常)
    // 断言:主异常为 BizException,suppressed 为关闭异常
}
#
★★

51. 多态在 Spring 注入(按类型)中的作用与坑点

多态在 Spring 注入(按类型)中的作用是什么?有哪些坑点?

  • Spring 按类型注入与多态
  • 多实现时的歧义(@Primary/@Qualifier)
  • 多态与依赖注入

Spring 依赖注入默认按类型(byType)查找 Bean,多态意味着一个接口由多个实现类提供时,Spring 需要从中选择一个或所有。作用:多态让接口定义契约、实现类可替换,Spring 按接口类型注入具体实现,支持策略模式与可扩展性。坑点:1)同一接口有多个 Bean 时,按类型注入会歧义(NoUniqueBeanDefinitionException),需用 @Primary 指定默认或 @Qualifier 指定名字;2)注入 List 或 Map<String,Interface> 可注入所有实现,但需注意顺序与 Key;3)Spring 的代理(CGLIB/JDK)与 final 类冲突(final 类无法被 CGLIB 代理);4)循环依赖与多态接口的组合可能引发歧义。正确处理:明确接口契约,用 @Primary/@Qualifier 消歧,或用 List/Map 注入策略集合。

多态是依赖注入的基础,但 Spring 按类型注入在"多实现"时产生歧义,需用 @Primary/@Qualifier 明确选择,或用集合注入实现多态策略。

interface Payment { void pay(); }
@Component @Primary class WeChat implements Payment {}   // 默认
@Component class Alipay implements Payment {}
@Service
class OrderService {
    @Autowired Payment payment;            // 默认选 @Primary 的 WeChat
    @Autowired @Qualifier("alipay") Payment p2; // 指定名字
    @Autowired List<Payment> payments;     // 注入全部实现
}
#
★★

52. 多态的方法调用机制(静态分派与动态分派)

多态的方法调用机制是什么?静态分派与动态分派有何区别?

  • 静态分派(重载,编译期)
  • 动态分派(重写,运行期)
  • 方法调用字节码(invokevirtual/invokestatic)

静态分派(static dispatch)发生在编译期,根据"静态类型"决定调用哪个重载方法(overload);动态分派(dynamic dispatch)发生在运行期,根据"实际类型"决定调用哪个重写方法(override),通过虚方法表(vtable)实现。静态分派人眼/编译器确定,动态分派由 JVM 运行时按对象实际类型查找。例如 obj.method() 中,方法是重载时按 obj 的静态类型选择;是重写时按 obj 的实际类型进行调用。字节码层面:invokestatic/静态分派用编译期确定的方法,invokevirtual/invokeinterface 走动态分派查找虚方法。invokedynamic 用于 Lambda 等。动态分派是运行时多态的基础,静态分派是重载的基础。

重载是静态分派(编译期按静态类型),重写是动态分派(运行期按实际类型)。理解二者区别才能解释多态调用选中的方法。

class A { void m(A a) { System.out.println("A.m(A)"); }
         void m(Object o) { System.out.println("A.m(Object)"); } }
class B extends A { void m(A a) { System.out.println("B.m(A)"); } }
A ref = new B();
ref.m(ref);        // 重载:静态类型 A -> 选 m(A)  动态分派:实际类型 B -> B.m(A)
#
★★

53. 如何保留原始 cause、稳定错误码并避免泄露敏感实现信息

设计异常时如何保留原始 cause、稳定错误码并避免泄露敏感实现信息?

  • cause 链保留
  • 错误码设计
  • 信息脱敏(避免泄露堆栈/内部细节)

保留原始 cause:构造异常时把底层异常作为 cause 传入(new BizException(msg, e)),调用方通过 getCause() 追溯根因,同时日志记录完整 cause 链。稳定错误码:定义枚举/常量错误码(如 ORDER_NOT_FOUND=1001),不在异常消息里硬编码,便于客户端与监控按错误码处理。避免泄露敏感信息:向用户/客户端暴露的异常消息只放稳定错误码与通用提示,不暴露堆栈、SQL、内部路径、服务地址等实现细节;完整堆栈只记录在服务端日志。设计上:内部异常(含根因)与对外异常(脱敏)分离,或异常消息用占位符,由错误码映射到用户友好文案。

异常设计要兼顾"内部诊断"(cause 链 + 日志完整栈)与"外部契约"(错误码 + 脱敏消息)。用错误码作为稳定标识,脱敏消息对客户端,原始 cause 仅供内部。

public class BizException extends RuntimeException {
    private final String code;                 // 稳定错误码
    public BizException(String code, String message, Throwable cause) {
        super(message, cause);                 // 保留 cause
        this.code = code;
    }
}
// 对外返回:{"code":"ORDER_NOT_FOUND","message":"订单不存在"}
// 内部日志:完整堆栈 + cause 链
#
★★

54. 如何在 Spring 统一异常处理(@ControllerAdvice/HandlerExceptionResolver)中保留 cause

在 Spring 统一异常处理(@ControllerAdvice/HandlerExceptionResolver)中如何保留 cause?

  • @ControllerAdvice/@ExceptionHandler 捕获异常
  • 保留 cause 的方式
  • 日志记录 cause 链

@ControllerAdvice 配合 @ExceptionHandler 捕获异常时,原始异常通过方法参数注入(包括 getCause() 链),需保留 cause 需要注意:1)@ExceptionHandler 方法接收异常对象,可调用 getCause() 追溯根因并记录;2)捕获后构造对外响应时,不丢失 cause(如需记录到日志用 log.error("...", e) 记录完整 cause 链);3)若把异常包装成新的异常再抛出,需用 new WrapperException(msg, e) 传入 cause 保留链;4)为多个异常类型定义处理器,用 @ExceptionHandler 的参数类型区分。在 HandlerExceptionResolver 中同样可访问异常并保留 cause。核心:记录日志时用日志 API 的异常参数(log.error(msg, ex)),不要只 ex.getMessage() 丢失 cause。

@ExceptionHandler 能拿到原始异常,保留 cause 的关键是"日志 API 记录完整异常对象"与"包装时传入 cause",避免仅记录消息丢失根因。

@ControllerAdvice
class Handler {
    @ExceptionHandler(DataAccessException.class)
    ResponseEntity<String> handleDB(DataAccessException e) {
        log.error("DB error, root=" + e.getCause(), e);  // 记录完整 cause 链
        return ResponseEntity.status(500).body("db error");
    }
}
#
★★

55. 密封类(sealed class)的 permits 子类控制与模式匹配的穷尽性

密封类(sealed class)的 permits 子类控制如何工作?与模式匹配的穷尽性如何结合?

  • sealed class 与 permits
  • 子类的限制(final/sealed/non-sealed)
  • 模式匹配穷尽性

sealed class 用 permits 显式声明允许的直接子类,子类集合封闭且有限。permits 子类必须满足:1)与密封类在同一模块(或同一包,若不同模块);2)子类必须声明为 final、sealed 或 non-sealed(即不能是普通可继承类);3)子类必须直接继承密封类。这使编译器能枚举所有子类型。与模式匹配结合:对 sealed 类用 switch 表达式/instanceof 模式匹配时,若覆盖了所有 permitted 子类,编译器判定穷尽,无需 default,遗漏则报错。这保证类型安全与分支完整。permits 控制"谁可扩展",模式匹配穷尽性保证"所有扩展都被处理"。

sealed 用 permits 封闭子类型集,配合模式匹配的穷尽性检查,让"新增子类必须有对应分支"成为编译期约束,是代数数据类型风格的基石。

public sealed class Shape permits Circle, Square, Triangle {}
public final class Circle extends Shape {}
public non-sealed class Square extends Shape {}
public sealed class Triangle extends Shape permits Rt {}  // 子类也 sealed
#
★★

56. 异常与返回值(Result 对象)的取舍

异常与返回值(Result 对象)在错误处理上如何取舍?

  • 异常 vs Result 对象
  • 控制流与性能
  • 可读性与强制处理

异常适合"异常情况"(不可预见的错误、跨层传播、需要栈信息),但频繁抛异常有栈快照成本,且易被吞;Result 对象(如 Result<T, E>、Either、Optional 或自定义结果类型)把成功/失败作为值返回,强制调用方处理失败分支,避免异常开销,适合"预期可能失败"的常规操作(校验、解析、查找)。取舍:异常用于"真正的异常路径"(系统错误、无法恢复、需要中断),Result 用于"预期失败"(业务校验、可恢复的失败),避免用异常做控制流。Result 提高可读性与强制处理,但增加样板;异常在跨层传播与根因诊断上更自然。现代函数式风格倾向对"可预期的失败"用 Result/Optional,对"异常"用异常。

异常是"非预期错误"的传播机制,Result 是"预期失败"的显式表达。用异常做控制流成本高且易吞,用 Result 处理预期失败更安全、可强制处理。

// 预期失败用 Result
Result<Order, Error> find(String id) { ... }
Result<Order, Error> r = find(id);
if (r.isErr()) { handle(r.err()); }
// 异常用于异常路径
void submit() { try { remote(); } catch (NetworkException e) { throw new BizException(e); } }
#
★★

57. 异常信息的国际化与用户友好

异常信息如何做国际化与用户友好化?

  • 错误码映射用户文案
  • 国际化(i18n)资源
  • 技术消息与用户消息分离

异常信息的国际化与用户友好化要"技术消息与用户消息分离":异常/错误码内部用稳定错误码标识,不直接写死用户文案;用户友好的提示通过错误码映射到国际化资源(如 properties 或 MessageSource),按 locale 选择文案。做法:1)异常携带错误码(如 code + 参数占位符),而非最终文案;2)在返回层用 MessageSource/Gets 按错误码与 locale 查找文案,并用参数填充(如"订单 ${id} 不存在");3)技术细节(根因、堆栈)只记录日志,不展示给用户;4)对用户显示通用、可理解的提示,避免堆栈与内部术语。实现上可用 MessageSource 或自定义错误码 → 文案映射表。

关键是把"错误码"与"文案"解耦:异常只带错误码和参数,文案由 locale 驱动的资源层生成,实现多语言与用户友好,同时技术细节不泄露。

// 异常携带错误码与参数
throw new BizException("ORDER_NOT_FOUND", new Object[]{id});
// 返回层按 locale 解析文案
String msg = messageSource.getMessage("order.not_found", new Object[]{id}, locale);
// 用户看到:"订单 123 不存在";日志另记完整堆栈
#
★★

58. 异常吞咽(Swallow)与日志记录的常见反模式

异常吞咽(Swallow)与日志记录的常见反模式有哪些?

  • 空 catch(吞异常)
  • 只 log 不处理
  • 双重日志与错误日志姿势

常见反模式:1)空 catch 块(catch (Exception e) {})吞掉异常,不记录、不处理,导致问题无法发现;2)只 log 不处理(catch 后 log 但继续执行),有时合理但多数应区分;3)只 log 一半(log e.getMessage() 而丢失 cause 与堆栈);4)在 catch 里重复抛新异常但不保留 cause;5)在代理层重复日志(双重日志);6)用异常做控制流(高频抛异常)。正确做法:捕获后要么记录完整堆栈(log.error(msg, e))并处理,要么抛给上层;记录时用日志 API 的异常参数保留 cause;避免空 catch 与吞异常。若确实需要"忽略"(如清理失败),应明确注释并记录 warning 级别。

异常吞咽的核心危害是"隐藏问题",只记录消息则丢失根因。正确姿势是"记录完整堆栈 + 决定处理或传播",避免重复日志与空 catch。

// 反模式:空 catch
try { risky(); } catch (Exception e) {}   // 吞掉,无法排查
// 反模式:只记录消息
catch (Exception e) { log.error(e.getMessage()); }  // 丢失 cause 与堆栈
// 正确
catch (Exception e) { log.error("failed", e); throw new BizException(e); }
#
★★

59. 异常处理与日志(MDC)的协作

异常处理如何与日志(MDC)协作,以保留诊断上下文?

  • MDC(Mapped Diagnostic Context)
  • 异常处理中保留 traceId
  • 日志与诊断上下文

MDC(Mapped Diagnostic Context,常用于 Logback/SLF4J)提供线程级键值对,被日志框架自动附加到日志条目,用于贯穿请求的 traceId、userId 等诊断上下文。异常处理与 MDC 协作:在请求入口设置 MDC(如 MDC.put("traceId", id)),异常处理时日志自动带上 traceId,便于串联一次请求的完整日志与异常。注意:1)请求结束需清理 MDC(MDC.remove),避免污染线程池复用线程;2)异步任务(线程池/虚拟线程)不会自动继承 MDC,需显式传递(如把 MDC 复制到子任务或使用线程池的 MDC 传递装饰器);3)异常日志用 log.error 记录异常对象,结合 MDC 定位。协作核心是让"日志与异常"在同一个诊断上下文(traceId)下可串联。

MDC 让日志带上请求级上下文,异常处理时记录完整堆栈并保留 MDC,就能按 traceId 串联一次请求的完整链路;需注意线程复用与异步任务下的 MDC 传递。

// 入口:MDC.put("traceId", traceId);
try { process(); } catch (Exception e) {
    log.error("process failed", e);   // 日志自动带 traceId
} finally {
    MDC.remove("traceId");            // 清理,避免污染线程池
}
// 异步:把 MDC 传给子线程(复制 MDC 或装饰器)
#
★★

60. 异常处理中的 finally 块、return 与资源释放

异常处理中的 finally 块、return 与资源释放如何正确协作?

  • finally 的语义
  • finally 中 return 吞异常
  • 资源释放的正确姿势

finally 块无论是否发生异常都会执行,用于资源释放/清理。但要注意:finally 中的 return 会吞掉 try 中抛出的异常(覆盖返回值),finally 中抛出的异常会覆盖 try 的异常。资源释放的正确姿势:优先用 try-with-resources 自动关闭资源;若用 finally 手动释放,释放动作本身 try/catch 包裹以避免 finally 异常覆盖主异常。finally 中 return 是反模式(会丢弃 try 的异常与返回值)。正确做法:try 正常返回,finally 只做清理且不 return/不抛异常;若清理失败,记录或 addSuppressed。资源释放要保证"无论成功失败都释放"。

finally 保证"清理必执行",但 return 会吞异常、抛异常会覆盖主异常。用 try-with-resources 或"finally 内 try/catch 清理"避免覆盖主异常。

// 反模式:finally 中 return 吞异常
try { return doWork(); } finally { return 0; }  // 吞掉 doWork 的异常
// 正确:try-with-resources 自动关闭
try (Connection c = getConn()) { return c.query(); }
// 手动 finally 清理:用 try/catch 包裹清理
finally { try { resource.close(); } catch (Exception e) { log.warn("close failed", e); } }
#
★★

61. 异常指标按类名和错误码聚合时,如何控制标签基数并仍能定位最常见的失败根因

异常指标按类名和错误码聚合时,如何控制标签基数(cardinality)并仍能定位最常见的失败根因?

  • 监控指标标签基数
  • 异常类名与错误码聚合
  • 基数控制与根因定位

监控系统(如 Prometheus)对标签基数(label cardinality)敏感,基数过大会拖垮存储与查询。按异常类名聚合时,类名本身基数可控(类数量有限),但错误码/消息若含动态值(如订单号、变量)会造成高基数爆炸。控制策略:1)聚合标签用"异常类名 + 稳定错误码 + 操作名",不把动态参数(订单号、URL 参数)作为标签;2)错误码用枚举/常量而非动态消息;3)对高基数字段(如具体参数)只记录到日志而非指标标签;4)用"错误码 + 顶层操作"作为主标签,落地最常见失败根因再下沉到日志/明细查询;5)用 frontend 只保留必要维度,避免 time series 爆炸。定位根因:指标聚合给"哪个错误码/类最频繁",再结合日志与 traceId 下钻到具体根因。

指标标签基数要"有限且稳定",动态值不能进标签。用类名+错误码+操作聚合定位高频失败,根因下钻到日志明细,兼顾监控效率与可归因性。

// Prometheus 指标示例(标签基数可控)
// http_errors_total{error_code="ORDER_NOT_FOUND",op="createOrder",status="400"} 5
// 动态值(订单号)不进标签,只进日志
// 基数值 = 错误码数 × 操作数,可控
#
★★

62. 异常链(Exception Chaining)与 Throwable.getCause() 在多层包装时的调试成本与设计反思

异常链(Exception Chaining)与 getCause() 在多层包装时有什么调试成本?如何设计反思?

  • 异常链与 cause
  • 多层包装的调试成本
  • 设计反思(避免过度包装)

异常链通过 cause 让异常间形成"根因 → 包装"的链,getCause() 可追溯。但多层包装(A 包 B 再包 C)会带来调试成本:1)每次包装都生成新异常对象与栈快照,占用内存与 CPU(尤其高频);2)多套栈信息难以快速定位,开发需层层 getCause() 追溯;3)日志输出大量嵌套堆栈,可读性差。设计反思:避免过度包装——只在"语义边界"(如跨层、跨模块、需要转换错误类型)包装一次,并保留 cause;同一层内不要反复包装;对不需要转型的异常直接传播;用错误码/类型区分而非包装层级。鼓励"扁平化"异常设计:明确边界包装,减少无意义嵌套。

异常链是诊断工具,但多层包装增大栈开销与调试成本。反思是"只在语义边界包装一次、保留 cause、避免同一层重复包装",让链保持简洁。

// 反模式:同一层反复包装
catch (IOException e) { throw new Wrapper1(e); }        // 内层
catch (Wrapper1 e) { throw new Wrapper2(e); }           // 又包一层
// 正确:跨模块边界包装一次,保留 cause
catch (IOException e) { throw new PersistenceException(e); }  // 仅一次
#
★★

63. 异常链(cause)与 addSuppressed 在 try-with-resources 中的应用

异常链(cause)与 addSuppressed 在 try-with-resources 中如何应用?

  • cause 与 addSuppressed 的区别
  • try-with-resources 的 suppressed
  • 主异常与关闭异常

cause 表示"原始根因 → 包装异常"的纵向链,addSuppressed 表示"主异常被抑制的附加异常"(横向并列)。在 try-with-resources 中:try 块抛出的业务异常是主异常,资源 close() 抛出的异常会被 addSuppressed 附加到主异常上(不覆盖),主异常照常抛出。应用:1)主异常用 getCause() 追溯根因;2)用 getSuppressed() 访问被抑制的关闭异常;3)设计包装异常时用 cause 保留原始异常,关闭异常用 suppressed 保留。二者可结合:包装异常保留 cause(根因),try-with-resources 自动 addSuppressed(关闭异常)。区别:cause 是"原因链",suppressed 是"并列的伴随异常"。

cause 是纵向"为什么",suppressed 是横向"还发生了什么"。try-with-resources 用 suppressed 保存关闭异常,避免关闭异常覆盖主异常,getSuppressed() 可访问。

try (Resource r = new Resource()) {
    throw new BizException("main", cause);   // 主异常,可带 cause
} catch (BizException e) {
    e.getCause();      // 根因
    e.getSuppressed(); // 关闭异常(若 close 抛异常)
}
#
★★

64. 批量处理允许部分成功时,异常模型应如何表达失败项、可重试性与整体提交状态

批量处理允许部分成功时,异常模型应如何表达失败项、可重试性与整体提交状态?

  • 批量部分成功的建模
  • 失败项与可重试性
  • 整体提交状态

批量处理允许部分成功时,不应直接抛异常(会丢失已完成项),而应返回一个"结果对象"表达:成功项、失败项集合(每项含失败原因与是否可重试)、整体提交状态(全部成功/部分成功/全部失败)。设计:1)用 BatchResult 封装 items + failures,failures 含 errorCode 与 retryable 标志;2)整体状态由成功数/失败数推导(如 status = failures.isEmpty() ? SUCCESS : (successes.isEmpty() ? FAILED : PARTIAL));3)可重试性:失败项单独标记 retryable,供重试框架只重试失败项;4)若全部失败且不可部分提交,可抛领域异常。异常模型上,用"结果对象 + 每项错误信息"而非单一异常;若需通知全局失败,可抛聚合异常但要把部分成功的明细放结果里。

部分成功的语义"部分项成功、部分失败"本身是结果而非异常,用 BatchResult 表达失败项与可重试性,整体状态由成功/失败推导,避免异常丢失部分成果。

class BatchResult<T> {
    List<T> successes;
    List<Failure> failures;   // Failure{item, errorCode, retryable}
    Status status() {
        if (failures.isEmpty()) return Status.ALL_SUCCESS;
        return successes.isEmpty() ? Status.ALL_FAILED : Status.PARTIAL;
    }
}
#
★★

65. 抽象类与接口在服务实现上的选择准则(Spring 实践)

抽象类与接口在服务实现上的选择准则是什么?Spring 实践中如何选择?

  • 抽象类 vs 接口
  • 服务实现的选择准则
  • Spring 实践(面向接口)

抽象类用于"相关类的通用实现 + 状态字段 + 模板方法",接口用于"契约定义 + 多实现 + 行为的抽象"。Spring 实践中通常"面向接口"编码:定义接口定义契约,实现类实现之,Spring 按类型注入。选择准则:若有共享实现代码/状态/模板方法用抽象类;若只是定义行为契约、需要多实现/可替换/易于测试用接口。Spring 中:接口更利于代理(JDK 动态代理)、解耦、测试 mock;抽象类适合共享基类逻辑。注意:接口可多实现(class A implements I1, I2),抽象类单继承;Java 8 接口默认方法让接口也能提供实现。实践中服务层多面向接口(便于 @Transactional 代理、策略替换),实现共享逻辑可用抽象类或组合。

接口强调"契约与多态",抽象类强调"实现的复用与模板"。Spring 面向接口利于代理与测试,抽象类用于共享实现,二者可结合(接口 + 抽象基类)。

interface OrderService { Order create(OrderDto d); }      // 契约
@Service class OrderServiceImpl implements OrderService { // 实现
    public Order create(OrderDto d) { ... }
}
// 抽象类用于共享逻辑
abstract class BaseRepo { protected Logger log = ...; abstract void save(); }
#
★★

66. 抽象类与接口的差异、Java 8 默认方法的多继承冲突

抽象类与接口的差异是什么?Java 8 接口默认方法的多继承冲突如何解决?

  • 抽象类 vs 接口差异
  • 默认方法多继承冲突
  • 冲突解决规则

差异:抽象类可有构造器、实例字段、非抽象方法、私有状态,单继承;接口(Java 8+)可有默认方法/静态方法/私有方法,但无实例字段(常量除外),可多实现(多继承接口)。Java 8 默认方法让接口能提供实现,但引入多继承冲突:一个类实现多个接口且这些接口有同名默认方法时,需解决。规则:1)类中显式方法优先于接口默认方法;2)否则选最具体的接口默认方法;3)若仍歧义(两个无关接口),必须显式 override 并用 X.super.method() 指定。抽象类与接口的差异决定"分量":要状态/模板用抽象类,要契约/多实现用接口。

抽象类偏"实现复用与状态",接口偏"契约与多态"。默认方法带来多继承能力但也产生冲突,Java 用"类优先+最具体+显式解决"规则消除歧义。

interface A { default void m() { System.out.println("A"); } }
interface B { default void m() { System.out.println("B"); } }
class C implements A, B {
    public void m() { A.super.m(); }   // 显式解决冲突,调用 A 的实现
}
#
★★

67. 抽象静态方法为何不允许

抽象静态方法为何不被允许?

  • 静态方法不能抽象
  • 静态方法绑定与多态
  • 接口/抽象类的静态方法

静态方法是类级(绑定到类),抽象方法要求"子类实现"并参与多态(动态分派),二者矛盾:静态方法不参与动态分派(invokestatic 编译期绑定),无法被覆写,因此"抽象"(要求子类实现并运行时替换)没有意义——抽象静态方法无法实现多态,也就无法定义"子类必须实现"的语义。Java 8+ 允许接口/类有静态方法(具体实现),但静态方法不能是抽象方法。若想"子类必须提供静态方法",无法通过抽象方法实现,只能通过约定或实例方法实现(如抽象实例方法 + 静态工厂)。本质:静态方法类级绑定,抽象方法需要实例级多态,二者不兼容。

抽象方法的价值在于"运行期多态替换",静态方法绑定到类、无法覆写,故抽象静态方法无意义且不被允许。继承层面需要多态时用实例抽象方法。

abstract class Foo {
    // abstract static void m();  // 编译错误:静态方法不能抽象
    static void helper() { }         // 静态方法必须具体实现
    abstract void instance();        // 抽象方法需实例级
}
#
★★

68. 捕获 InterruptedException 后为什么通常需要恢复中断标记,忽略它会破坏哪些取消协议

捕获 InterruptedException 后为什么通常需要恢复中断标记?忽略它会破坏哪些取消协议?

  • InterruptedException 与中断标记
  • 恢复中断标记(Thread.currentThread().interrupt())
  • 取消协议

当线程被 interrupt() 时,阻塞方法(sleep/wait/join)抛 InterruptedException 并清除中断标记。若捕获后不恢复中断标记,调用方无法感知线程被中断,取消请求被"吞掉",破坏协作式取消协议。恢复中断标记:捕获后调用 Thread.currentThread().interrupt() 重新设置标记,让上层代码(或 finally 中检查 isInterrupted())能感知中断并正确退出。若捕获后立即返回且不恢复,则中断被静默忽略,可能造成线程继续执行本应取消的任务,或线程池中被中断的线程状态错误。唯一例外:若方法自己的契约就是"吞掉中断并结束"(如确实完成清理后退出),才可不恢复,但通常应恢复。理解:中断是协作式,标记需传递。

中断是协作式取消机制,InterruptedException 捕获后标记被清除,恢复标记是"把中断意图传递给上层",否则取消请求被吞,破坏协作取消协议。

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();   // 恢复中断标记,传递取消意图
    throw new RuntimeException(e);        // 或退出
}
#
★★

69. 接口中的 private 方法(Java 9+)与默认方法的协作

接口中的 private 方法(Java 9+)如何与默认方法协作?

  • 接口 private 方法(Java 9+)
  • 与默认方法共享代码
  • 接口私有方法的限制

Java 9+ 允许接口定义 private 方法(静态或实例级),用于提取接口内部共享的代码,尤其是多个默认方法之间的公共逻辑。private 实例方法可被默认方法调用,private static 方法可被静态方法/默认方法调用。与默认方法协作:默认方法往往有重复实现,用 private 方法抽取公共逻辑,避免重复,同时不暴露给外部(保持接口 API 简洁)。限制:接口 private 方法不能是 abstract(必须有实现),只能在接口内部使用,不能被外部实现类调用;接口私有方法不能用于提供多态(不参与覆写)。它让接口实现更干净、可维护。

接口 private 方法把"接口内部共享实现"封装起来,避免默认方法重复代码,同时不增加对外 API 面,是接口内部复用与默认方法协作的机制。

interface Loggable {
    default void logInfo(String m) { format("INFO", m); }
    default void logError(String m) { format("ERROR", m); }
    private void format(String level, String m) {   // 接口私有方法,共享逻辑
        System.out.println(level + ": " + m);
    }
}
#
★★

70. 接口常量、枚举常量与 final 字段的使用边界

接口常量、枚举常量与 final 字段的使用边界是什么?

  • 接口常量(implicitly public static final)
  • 枚举常量
  • final 字段

接口常量是接口中的 public static final 字段(隐式),但被普遍视为反模式,因为接口常量会把常量暴露给所有实现类并污染命名空间,且无类型边界。枚举常量(enum 常量)用于表示有限的固定值集合,类型安全、可携带行为,是常量集的最佳选择。final 字段用于类中不可变常量(public static final 或实例 final)。使用边界:需要有限枚举值用 enum;需要模块级/类级常量用 final static 字段;避免在接口中定义常量(应使用 enum 或专门的常量类)。接口常量违背接口"契约"语义,应避免。

接口常量是历史遗留,把常量塞进接口污染 API 与命名;enum 提供类型安全的有界常量,final 字段适合类常量。边界在于"有界值集合用 enum,通用常量用 final 字段,接口避免放常量"。

// 反模式:接口常量
interface Foo { int MAX = 100; }   // implicitly public static final,污染命名空间
// 推荐:enum 有界常量
enum Status { ACTIVE, INACTIVE }   // 类型安全
// 或 final 常量类
final class C { static final int MAX = 100; }
#
★★

71. 文本块(Text Block,Java 13+)的缩进与转义规则

文本块(Text Block,Java 13+)的缩进与转义规则是什么?

  • 文本块语法(三引号)
  • 缩进处理(incidental/essential whitespace)
  • 转义(\s、\n)

文本块(Text Block,Java 13 预览、Java 15 定稿)用三个双引号 """ 包裹多行字符串,自动处理换行与缩进。缩进规则:编译时去除"共同缩进"(incidental whitespace),保留相对缩进(essential whitespace)——即所有行共享的最小缩进被移除。转义:\s 表示显式空格,\n 等转义可控制换行,尾部 \ 可续行。文本块用于多行文本(SQL、JSON、HTML),避免手动转义与拼接。注意:文本块是编译期常量,可被拼接;开头换行(第一个换行)被自动跳过。缩进也可用 indent() 方法调整。

文本块的核心是"不用转义处理多行文本",缩进按"共同缩进"剥离,让代码缩进与运行时文本缩进解耦,转义符提供精确控制。

String sql = """
    SELECT id, name
    FROM users
    WHERE age > 20
    """;
// 输出:SELECT id, name\nFROM users\nWHERE age > 20
String s = "a\s b";   // \s 显式空格
#
★★

72. 构造器链、this() 与 super() 的执行顺序

构造器链中 this() 与 super() 的执行顺序是怎样的?

  • this() 与 super() 的调用限制
  • 构造器链的执行顺序
  • 初始化顺序

构造器中 this(...) 调用同类另一构造器,super(...) 调用父类构造器,二者都必须出现在构造器第一行,因此不能同时出现(this 最终会间接调用 super)。执行顺序:构造器链是"从父类到子类"逐层执行——super() 先执行父类构造器(初始化父类字段),再执行子类构造器。若用 this(...) 委托,则先执行委托的构造器(它内部先 super()),再执行当前构造器剩余部分。实例初始化顺序:静态初始化块(类加载时)→ 实例字段初始化与实例初始化块 → 构造器(先 super 后子类体)。子类构造器默认隐式调用 super()(无参),若父类无无参构造器则需显式 super(参数)。

构造器链保证"父类先初始化、子类后初始化",this()/super() 必须第一行保证委托链接完整。理解初始化顺序是正确使用继承构造器的基础。

class Base { Base() { System.out.println("Base"); } }
class Sub extends Base {
    Sub() { this(1); System.out.println("Sub()"); }
    Sub(int x) { super(); System.out.println("Sub(int)"); }  // 先 super 再本构造器
}
// 执行顺序:Base -> Sub(int) -> Sub()  (this 委托后执行当前构造器)
#
★★

73. 注解(Annotation)在 JDK 8 起的类型注解(Type Annotation)

注解在 JDK 8 起的类型注解(Type Annotation)是什么?有什么应用?

  • Type Annotation(@Target(TYPE_USE))
  • 注解可作用于类型
  • 应用(空值检查、检查器框架)

JDK 8 引入类型注解(Type Annotation),允许注解作用于类型出现的位置(而不仅是声明),通过 @Target(ElementType.TYPE_USE) 声明。类型注解可放在泛型参数、数组、类型转换、new、instanceof、throws 等类型位置,如 List<@NonNull String>、(@NonNull Object) obj。应用:空值检查(@NonNull/@Nullable)、可空性分析、检查器框架(Checker Framework)、类型断言等。它让注解能够描述"类型本身的属性"而非"声明的属性"。类型注解与声明注解(@Target(TYPE) 等)区分为两个维度,可同时存在。

类型注解把注解能力从"声明"扩展到"类型出现的任何位置",用于编译期类型检查(可空性、不可变等),是 Checker Framework 的基础。

@Target(ElementType.TYPE_USE)
@interface NonNull {}
List<@NonNull String> list;      // 类型注解作用于泛型参数
String s = (@NonNull String) obj; // 作用于类型转换
#
★★

74. 浮点精度与 BigDecimal 在金融计算中 scale 与舍入模式(RoundingMode)的取舍策略有哪些

在金融计算中,BigDecimal 的 scale 与舍入模式(RoundingMode)有哪些取舍策略?

  • BigDecimal 的 scale 与精度
  • RoundingMode 的选择
  • 金融计算的精度策略

金融计算必须用 BigDecimal(或金额整数化),避免浮点误差。scale(小数位数)决定精度,如 scale=2 表示两位小数。取舍策略:1)金额存储统一用固定 scale(如 2 位或 4 位),存入数据库用 DECIMAL,避免 scale 漂移;2)舍入模式(RoundingMode)决定舍入行为:HALF_UP(四舍五入)常用于金额结算,HALF_EVEN(银行家舍入)用于统计/分摊避免系统性偏差,UP/DOWN 用于固定方向,FLOOR/CEILING 用于保底;3)乘法/除法结果 scale 需显式指定 setScale(scale, RoundingMode),除法要指定 scale 否则抛 ArithmeticException(非整除);4)比较用 compareTo(忽略 scale),不用 equals(equals 要求 scale 相同)。策略:确定统一 scale 与 RoundingMode,构造用 String(new BigDecimal("0.1"))而非 double(避免二进制误差)。

金融精度核心是"统一 scale + 明确 RoundingMode + 用 String 构造 + compareTo 比较"。HALF_EVEN 减少统计偏差,HALF_UP 直观,分摊用特殊模式。

BigDecimal a = new BigDecimal("0.10");          // 用 String 构造避免 double 误差
BigDecimal b = new BigDecimal("0.20");
BigDecimal sum = a.add(b).setScale(2, RoundingMode.HALF_UP);
BigDecimal avg = sum.divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_EVEN);
System.out.println(a.compareTo(b));              // 比较用 compareTo,忽略 scale
#
★★

75. 浮点精度与 BigDecimal 的正确使用(构造、scale、等值比较)

浮点精度与 BigDecimal 的正确使用是什么?包括构造、scale、等值比较?

  • new BigDecimal(double) vs String
  • scale 与精度
  • equals vs compareTo

浮点(double)用二进制表示,很多十进制小数(如 0.1)无法精确表示,产生误差。BigDecimal 的正确使用:1)构造:用 new BigDecimal("0.1")(String)或 BigDecimal.valueOf(0.1),避免 new BigDecimal(0.1)(double 构造会引入二进制误差,如 0.1 变成 0.1000000000000000055511151231257827);2)scale:小数位数,若需固定精度用 setScale(scale, mode);3)等值比较:用 compareTo(忽略 scale,0.10 与 0.1 相等),不用 equals(equals 要求 scale 相同,0.10 != 0.1);4)除法需指定 scale(避免非整除抛 ArithmeticException)。浮点适合科学与性能场景,金额/精确计算用 BigDecimal。

关键陷阱是 double 构造与 equals 比较。用 String 构造保证精确,compareTo 比较忽略 scale,setScale 控制精度。

BigDecimal bad = new BigDecimal(0.1);      // 0.1000000000000000055511151231257827,误差
BigDecimal good = new BigDecimal("0.1");   // 0.1,精确
BigDecimal x = new BigDecimal("0.10"), y = new BigDecimal("0.1");
x.equals(y);        // false(scale 不同)
x.compareTo(y);     // 0(忽略 scale,相等)
#
★★

76. 继承层级过深的实际危害与重构方法

继承层级过深的实际危害是什么?如何重构?

  • 深继承的危害
  • 破坏封装与可维护性
  • 重构方法(组合/接口)

继承层级过深的危害:1)耦合高,子类依赖整个祖先链,修改父类影响所有子类;2)可读性差,行为分散在多层,难以理解;3)破坏封装(子类可访问/覆写父类内部);4)脆弱的基类问题(基类修改引发子类意外行为);5)组合爆炸(多维度变化需要大量子类)。重构方法:1)用组合代替继承(把可变行为委托给接口/策略对象);2)用接口定义契约,实现类组合;3)把共享逻辑下移到"工具类/组合对象"而非多层继承;4)用模板方法 + 接口,扁平化层级;5)对行为差异用策略模式/装饰器。原则:保持层级浅(2-3 层内),优先组合。

深继承的代价是耦合与脆弱性,重构趋势是"组合优于继承"——用接口契约 + 委托复用,扁平化层级,提高可维护性。

// 反模式:深继承 A->B->C->D
// 重构:组合
class Service {
    private final Behaviour behaviour;   // 委托行为
    Service(Behaviour b) { this.behaviour = b; }
    void run() { behaviour.apply(); }
}
interface Behaviour { void apply(); }
#
★★

77. 自定义异常跨模块公开时,如何设计构造器、错误码和序列化边界以保持版本兼容

自定义异常跨模块公开时,如何设计构造器、错误码和序列化边界以保持版本兼容?

  • 跨模块异常构造器设计
  • 错误码与序列化
  • 版本兼容(serialVersionUID)

自定义异常跨模块公开(作为 API 的一部分)时,需保持版本兼容:1)构造器:提供稳定、向后兼容的构造器集合(无参、带消息、带 cause 等),新增构造器时保留旧构造器,避免破坏调用方;2)错误码:用稳定枚举/常量,错误码不随版本变化,新增错误码不删除旧码;3)序列化:显式声明 serialVersionUID(固定值),字段变化时保持序列化兼容;新增字段影响兼容性(同版本需兼容,跨版本需考虑 serialVersionUID 与 readObject/readResolve);4)接口边界:异常作为公开 API 时避免暴露实现细节,用稳定的 message 与 code。设计原则:错误码是稳定契约,构造器向后兼容,序列化用显式 serialVersionUID 并谨慎增删字段。

跨模块公开异常 = 异常成为 API 契约,需稳定错误码、向后兼容构造器、显式 serialVersionUID 与序列化边界,避免破坏下游使用方。

public class BizException extends RuntimeException {
    private static final long serialVersionUID = 1L;   // 固定,保持序列化兼容
    private final String code;   // 稳定错误码
    public BizException(String code) { this(code, null, null); }      // 向后兼容构造器
    public BizException(String code, String message) { this(code, message, null); }
    public BizException(String code, String message, Throwable cause) { ... }
}
#
★★

78. 记录类(record)与普通类的差异,哪些场景不应使用

记录类(record)与普通类有何差异?哪些场景不应使用 record?

  • record 的不可变与自动生成方法
  • record 与普通类差异
  • 不应使用 record 的场景

record(Java 16 定稿)是不可变数据载体:自动生成构造器、equals/hashCode/toString、访问器(component 方法),字段 final,无 setter,类 final。差异:record 更简洁(一行定义数据类)、不可变、自动实现值语义;普通类可自由定义可变性、继承、构造器逻辑。不应使用 record 的场景:1)需要可变状态(record 不可变);2)需要继承/扩展(record 是 final,不能继承,只可实现接口);3)需要自定义 equals/hashCode 语义(如业务键)以外的复杂逻辑;4)需要大量额外字段/方法(record 主要承载数据);5)需要抽象类继承。record 适合"纯数据载体"(DTO、返回值、请求响应),不适合复杂业务对象。

record 是"不可变数据载体",自动生成值语义方法;需要可变、继承、复杂行为时用普通类。record 简化 DTO,但不能替代所有类。

record Point(int x, int y) {}   // 自动生成构造器、equals/hashCode/toString、x()/y()
Point p = new Point(1, 2);
p.x();            // 访问器
new Point(1,2).equals(new Point(1,2)); // true 值语义
// 不可用于需要可变状态的对象
#
★★

79. 访问修饰符(public/protected/package-private/private)

请说明 Java 访问修饰符(public/protected/package-private/private)的访问范围?

  • 四种访问修饰符
  • 访问范围(类内/包内/子类/全局)
  • protected 的语义

四种访问修饰符:1)public:任何类都可访问;2)protected:同包内可访问,跨包仅子类可访问(且通过继承);3)package-private(默认,无修饰符):仅同包内可访问;4)private:仅本类内可访问。访问范围从宽到窄:public > protected > package-private > private。protected 的语义:同包 + 子类(跨包通过继承关系访问)。适用:封装内部实现用 private,包内协作用 package-private,扩展 API 用 protected,对外契约用 public。访问修饰符是实现封装与模块边界的基础。

访问修饰符控制"可见性边界",private 封装实现、public 公开契约、protected 开放扩展、package-private 包内共享。合理使用是良好封装的基石。

public class A {
    public int pub;              // 全局可见
    protected int prot;          // 同包 + 跨包子类
    int pkg;                     // 同包可见(package-private)
    private int priv;            // 仅本类
}
#
★★

80. JDK 21 弃用、Windows 32 位移除与未来删除 API 的应对策略

面对 JDK 21 弃用、Windows 32 位移除与未来删除 API,应如何应对?

  • JDK 弃用与移除
  • Windows 32 位支持移除
  • 应对策略

JDK 持续演进,API 会被弃用(deprecated)并最终移除(remove)。JDK 21 弃用一些 API(如 Thread.stop 等),且后续版本移除 Windows 32 位支持(JDK 21 起不再支持 Windows 32 位)。应对策略:1)关注编译期弃用警告(@Deprecated 与 -Xlint:deprecation),尽早迁移;2)规划升级路径,避免在移除日期后仍使用旧 API;3)依赖管理:检查第三方库对 JDK 版本的要求,避免旧库使用已移除 API;4)平台支持:确认部署平台(Windows 32 位)是否被目标 JDK 支持,必要时升级 OS 或使用受支持版本;5)用 jdeps、文档(JEP)跟踪弃用与移除计划;6)对新代码避免引入已弃用 API。策略核心是"及时迁移 + 保持依赖与平台兼容"。

JDK 移除 API 是不可避免的演进,应对是"跟踪弃用警告、制定升级计划、核对依赖与平台支持",避免被移除打个措手不及。

// 编译时发现弃用警告
// javac -Xlint:deprecation App.java
// 避免使用已弃用 API(如 Thread.stop()、finalize() 等)
// 部署时确认 JDK 对平台(Windows 32 位)的支持
#
★★

81. JDK 25 中 try 语句对结构化并发(Structured Concurrency)抛出的取消异常传播机制如何处理

JDK 25 中 try 语句对结构化并发抛出的取消异常传播机制如何处理?

  • 结构化并发与传统 try
  • 取消异常传播
  • 虚拟线程与作用域

结构化并发(StructuredTaskScope)中,子任务抛出的异常(含取消异常)在作用域内传播。JDK 25 的 try-with-resources 与 StructuredTaskScope 结合:scope 作为 AutoCloseable,在 try 结束后关闭;若子任务被取消(shutdown)或抛异常,scope.join() 会抛出相应异常(如 CancellationException 或聚合异常)。取消异常传播机制:StructuredTaskScope 的 join() 在子任务失败时抛异常并传播取消意图,调用方在 try 中捕获并处理;try 语句保证 scope 的关闭(close)总是执行,即使有取消异常。关键是把"并发子任务"作为整体单元,取消/异常在作用域统一传播,try-with-resources 确保资源(scope)关闭。

结构化并发把并发任务纳入作用域生命周期,join() 汇总子任务结果/异常,取消通过 shutdown 传播,try-with-resources 保证 scope 关闭,形成统一的异常与取消传播机制。

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> a = scope.fork(() -> callA());
    Future<String> b = scope.fork(() -> callB());
    scope.join();          // 任一失败抛异常(含取消)
    return a.resultNow() + b.resultNow();
} // try 结束自动关闭 scope,即使有取消异常
#
★★

82. switch 对 String 的匹配是如何编译实现的(hashCode 比较后 equals 确认),与整数 switch 的 tableswitch/lookupswitch 有何差异

switch 对 String 的匹配是如何编译实现的?与整数 switch 的 tableswitch/lookupswitch 有何差异?

  • String switch 的编译实现(hashCode + equals)
  • tableswitch/lookupswitch
  • 差异

switch 对 String 的匹配编译为:先计算字符串的 hashCode,用 hash 定位一个候选 case(通过 lookup 或 filter),再对命中的 case 用 equals 确认(因为 hash 可能冲突),从而保证精确匹配。整数 switch 编译为 tableswitch(case 值连续时用跳转表,O(1))或 lookupswitch(case 值稀疏时用二分查找,O(log n))。String switch 适合匹配 String 值,但编译更复杂(hash + equals);整数 switch 直接按值跳转,性能更高。差异:String switch 需 hashCode 计算 + equals 确认,整数 switch 用 tableswitch/lookupswitch 直接跳转。String switch 不允许 null(会 NPE),需先判空。

String switch 用 hash 粗略定位 + equals 精确确认,避免对每个 case 都 equals;整数 switch 用跳转表/二分查找。String switch 是语法糖,编译为 hash 分发。

String s = "b";
switch (s) {            // 编译为:hashCode 定位 + equals 确认
    case "a" -> ...;
    case "b" -> ...;
    default -> ...;
}
// 整数 switch:tableswitch(连续)/ lookupswitch(稀疏)
#
★★

83. 匿名内部类与 Lambda 在 this 指向、变量捕获与字节码生成上的差异

匿名内部类与 Lambda 在 this 指向、变量捕获与字节码生成上有何差异?

  • this 指向差异
  • 变量捕获
  • 字节码生成(invokedynamic vs 内部类)

匿名内部类与 Lambda 的差异:1)this 指向:匿名内部类中 this 指向内部类实例本身;Lambda 中 this 指向外部类(Lambda 没有自己的 this,与外层一致)。2)变量捕获:两者都只能捕获有效 final 的局部变量;但匿名内部类可以用 this 访问外部实例,额外捕获外部实例引用。3)字节码生成:匿名内部类编译为独立的 class 文件(内部类),携带外部实例引用,每次实例化分配对象;Lambda 用 invokedynamic + LambdaMetafactory 生成,通常复用同一实现(无独立类文件,惰性生成),不强制捕获外部实例引用(除非用到)。差异导致 Lambda 更轻量、语义更贴近函数式,匿名内部类更"完整类"。

关键差异是 this 指向(Lambda 里 this 是外层)与实现机制(Lambda 用 invokedynamic 轻量生成,匿名类有独立 class 文件)。Lambda 更推荐用于函数式接口。

class Outer {
    Runnable r1 = () -> System.out.println(this);   // this = Outer 实例
    Runnable r2 = new Runnable() { public void run() { System.out.println(this); } }; // this = 匿名内部类
}
#

84. JDK-8213077(异常忽略 umbrella bug)的成因与防御性异常处理启示

JDK-8213077(异常忽略 umbrella bug)的成因是什么?带来哪些防御性异常处理启示?

  • 异常忽略的成因
  • 防御性异常处理
  • 实践启示

JDK-8213077 是"异常忽略(exception swallowing)"类问题的汇总 bug,指代码中大量存在捕获异常后忽略(空 catch 或只记消息)导致难以排查。成因:开发者为快速通过编译或避免繁琐处理而捕获异常后不处理,或只记录 e.getMessage() 丢失 cause 与堆栈,导致故障被隐藏、定位困难。启示:1)不空 catch,捕获后至少记录完整堆栈(log.error(msg, e));2)区分"可忽略"与"不可忽略"异常,可忽略的明确注释并记录低级别日志;3)保留 cause 链,不丢失根因;4)用错误码/日志规范统一异常记录;5)代码审查与静态检查(如 Checkstyle/SpotBugs 检测空 catch)防御。核心启示是"异常不应当被静默忽略,应记录或传播"。

异常忽略掩盖 bug,JDK-8213077 是系统性警示。防御性做法是"记录完整堆栈 + 明确忽略理由 + 保留 cause + 静态检查",让异常可见、可诊断。

// 反模式:空 catch / 只记消息
catch (Exception e) { log.error(e.getMessage()); }   // 丢失 cause 与堆栈
// 防御:记录完整堆栈 + 明确处理
catch (Exception e) { log.error("operation failed", e); throw new BizException(e); }
#

85. Java 24/25 中 JEP 495(第 4 次预览)到 JEP 512(终版)的“更简洁源文件与实例 main”演进边界

Java 24/25 中 JEP 495(第 4 次预览)到 JEP 512(终版)的"更简洁源文件与实例 main"演进边界是什么?

  • JEP 495(第 4 次预览)
  • JEP 512(终版)
  • 简化 main 方法的演进

"更简洁源文件与实例 main"(Simple Source Files and Instance Main)旨在简化 Java 入门:未命名的类和实例 main 方法(无需 public static void main(String[]) 样板)。JEP 495 在 JDK 24 作为第 4 次预览,JEP 512 在 JDK 25 转终版。演进边界:允许源文件不声明类(隐式未命名类),main 方法可以是实例方法(非 static),方法签名可省略参数(如空参 main),隐式类不能声明为 public。终版比预览更稳定:统一了 main 的启动语义、补全了未命名类与实例 main 的规则、移除预览限制。它让单文件程序更简洁,减少样板代码,方便初学者与脚本用途。

JEP 495→512 是"未命名类 + 实例 main"从预览到终版的演进,简化了 Java 程序的书写,但同样保留完整语言语义(隐式类有边界,如不能 public 包级)。

// JEP 512 终版:简洁源文件
void main() {          // 实例 main,无需 public static
    System.out.println("Hello");
}
// 未命名类 + 简洁 main,无需 class 声明样板
#

86. Java 中值类(Value Class)提案(Project Valhalla / JEP 401 方向)

Java 中值类(Value Class)提案(Project Valhalla / JEP 401 方向)是什么?

  • Project Valhalla
  • 值类(Value Class)概念
  • 值语义与性能

Project Valhalla 的 JEP 401 方向是引入值类(Value Classes),让开发者定义"基于值语义"的类:值类不支持引用同一性(无 == 比较),实例按值处理,可被 JVM 扁平化存储(无需对象头),减少内存与 GC 压力,提升性能(尤其集合与小对象)。值类与 record/不可变结合,提供"值语义的类",可用于高性能计算与集合。与普通类区别:值类无身份(identity),不能用于 synchronized、== 比较、hashCode 按值而非地址。JEP 401 经过多次演进(value objects 早期提案),最终方向是"值类 + 原始类型类"等。它让 Java 获得类似 "struct" 的性能优势同时保持面向对象。

值类是 Project Valhalla 的核心,消除对象身份开销,让不可变类按值扁平化存储,提升性能。仍处于演进,需理解"值语义 vs 身份语义"。

// 值类方向(示例,JEP 401 演进中)
public value class Point {   // 值类:无身份,按值语义
    private final int x, y;
    public Point(int x, int y) { this.x = x; this.y = y; }
}
// 扁平化存储,无对象头,== 比较值语义
#

87. JDK 25 引入的 JEP 513 灵活构造函数体(终版,前身为 JDK 23/24 的 JEP 482/492 预览)对异常传播路径的影响是什么

JDK 25 的 JEP 513 灵活构造函数体(终版,前身为 JEP 482/492 预览)对异常传播路径有什么影响?

  • JEP 513 灵活构造函数体
  • 语句先于 this()/super()
  • 异常传播路径

JEP 513 灵活构造函数体(Flexible Constructor Bodies)允许构造函数中在 this()/super() 之前执行语句(如参数校验、前置处理),此前 super()/this() 必须是第一行。影响:在 super() 之前执行的语句(如参数校验)抛出的异常,其传播路径与之前不同——之前 super 必须第一行,校验只能在 super 之后;现在可在 super 前校验,异常在父类构造前抛出,此时对象尚未初始化父类部分,异常直接传播给调用方,父类构造器不会执行。这影响:1)参数校验提前到 super 前,失败时父类不构造;2)异常抛出的时机更早,传播路径更清晰;3)super 前语句不能访问 this 的字段(未初始化完整)。终版让构造器更灵活并可提前校验。

JEP 513 允许 super()/this() 前执行语句,使参数校验可提前到父类构造前,异常传播路径随之改变(父类不初始化即失败),提升构造器灵活性。

class Sub extends Base {
    Sub(int x) {
        if (x < 0) throw new IllegalArgumentException("x<0");  // super 前校验
        super(x);   // 之后才调用父类构造
        // 校验异常在父类构造前抛出,直接传播
    }
}
#

88. JEP 511 Module Import Declarations 在 JDK 25 对源文件头部 import 模块的影响

JEP 511 Module Import Declarations 在 JDK 25 对源文件头部 import 模块有什么影响?

  • JEP 511 模块导入
  • import 模块声明
  • 简化模块导入

JEP 511(Module Import Declarations)在 JDK 25 引入模块导入声明,允许在源文件头部用 import module <模块名>; 导入整个模块,从而无需逐个 import 该模块导出的所有包。这简化了大型模块化代码的导入:import module java.base; 即可使用 java.base 导出的所有类型,避免长串 import 语句。影响:减少样板 import、提高可读性,但需注意通配符导入模块可能引入命名冲突(与普通 import 冲突时需显式导入)。它主要用于简化模块化应用/模块的书写,与类路径下逐包 import 互补。模块导入是编译期概念,不影响运行时模块图。

模块导入(import module)批量导入模块导出的所有包,简化头部 import,但潜在命名冲突需显式 import 解决,是编译期便捷特性。

// JEP 511 模块导入
import module java.base;   // 导入 java.base 模块导出的所有包
import module java.sql;    // 可同时导入多个模块

public class App {
    // 可直接使用 java.base 中的类型,无需逐个 import
}
#

89. JEP 513 Flexible Constructor Bodies 在 JDK 25 允许构造函数中语句先于 this()/super() 的边界

JEP 513 Flexible Constructor Bodies 在 JDK 25 允许构造函数中语句先于 this()/super() 的边界是什么?

  • JEP 513 的边界
  • super 前允许的语句
  • 限制

JEP 513 允许构造函数中在 this()/super() 之前执行语句,但边界与限制:1)super()/this() 前不能访问 this 的实例字段或调用实例方法(对象未初始化,字段可能未赋默认值/非法);2)super 前语句也不能使用 super 的成员(父类未构造);3)不能把 super()/this() 放在 finally 或 try-with-resources 等可能导致"跳过"调用的位置(必须保证 super 恰好调用一次);4)super 前可执行参数校验、局部计算、设置局部变量等安全语句;5)super 调用后构造器行为照常。边界本质:super 前只能做"不依赖 this/父类状态"的语句,保证父类初始化完整性。

JEP 513 的边界是"super 前不能访问 this 或父类状态、不能放在可能跳过 super 的结构中",保证父类恰好初始化一次且安全。

Sub(int x) {
    if (x <= 0) throw new IllegalArgumentException();  // 允许:不依赖 this
    int y = x * 2;                                     // 允许:局部计算
    // this.field = x; // 不允许:super 前不能访问 this
    super(x);                                          // 必须保证恰好一次
}
#

90. Java 9 引入的字符串紧凑字符串(Compact Strings)

Java 9 引入的字符串紧凑字符串(Compact Strings)是什么?

  • Compact Strings 原理
  • LATIN-1 与 UTF-16 存储
  • 内存优化

Compact Strings(JEP 254,Java 9)优化 String 存储:之前 String 用 char[](UTF-16,每字符 2 字节);现在 String 内部用 byte[] + coder 标志,若字符都在 LATIN-1 范围(0-255),用 1 字节/字符存储,否则用 UTF-16(2 字节/字符)。coder 字段标记编码方式。这让"纯 ASCII/拉丁字符"的字符串内存占用减半(约 50% 压缩率),提升内存利用率与缓存效率。String 的 API 行为不变(逻辑字符仍按 UTF-16 code unit 处理),只是内部存储优化。对中文等非 LATIN-1 字符仍用 2 字节。Compact Strings 是 JVM 透明优化,开发无需感知,但理解它有助于理解内存占用。

Compact Strings 用 byte[] + coder 按需选择 1 或 2 字节/字符,ASCII 字符串内存减半,是 JDK 内部存储优化,API 语义不变。

// 内部:String 用 byte[] + byte coder
// coder == LATIN1 (1 字节/字符) 或 UTF16 (2 字节/字符)
String ascii = "hello";   // 用 LATIN-1,每字符 1 字节
String cn = "中文";        // 用 UTF-16,每字符 2 字节
// 开发无感知,API(length 等)保持 UTF-16 code unit 语义
#

91. Java 中 strictfp 的历史与现状

Java 中 strictfp 的历史与现状是什么?

  • strictfp 的历史
  • FP 严格性
  • 现状(JDK 17 恢复)

strictfp 关键字强制浮点运算遵循 IEEE 754 严格规范(不利用扩展精度),保证跨平台浮点结果一致;非 strictfp 允许在某些平台(x86)用扩展精度提升性能但结果可能略有差异。历史:Java 1.2 引入 strictfp,早期默认"非严格"(允许扩展精度),strictfp 用于类/方法;随着行业默认 IEEE 严格,JDK 17(JEP 306)恢复 strictfp 语义为默认——即所有浮点运算默认严格遵循 IEEE 754,strictfp 关键字成为恒等操作(无实际影响但仍保留)。现状:Java 17 起默认严格 IEEE 754,strictfp 不再需要,属于历史遗留但保留兼容。

strictfp 保证跨平台浮点一致性,JDK 17 起默认严格 IEEE 754,strictfp 成为无操作保留,是历史语义的演变。

strictfp class Calc {   // JDK 17 前用于强制 IEEE 严格;17 后默认即严格,无实际影响
    double sum(double a, double b) { return a + b; }
}
#

92. Java 模式匹配的 instanceof、模式变量与 switch 表达式在 JDK 25 中的作用域规则有何更新

Java 模式匹配的 instanceof、模式变量与 switch 表达式在 JDK 25 中的作用域规则有何更新?

  • 模式变量作用域
  • instanceof 与 switch 的作用域
  • JDK 25 更新

模式匹配中,模式变量(instanceof 后的变量、switch case 中的模式变量)的作用域规则决定其使用范围。JDK 21 定稿后,JDK 25 进一步细化作用域:instanceof 模式变量在"流经"(flowing)的分支内可用(如 if 的 true 分支);switch 表达式/语句中模式变量在对应 case 块内可用。更新与演进:模式变量作用域从"匹配后所在块"细化为"流经分析",保证只有确实匹配后才可用,避免在未匹配路径使用;switch 模式变量在 case 内作用域;record 解构模式变量同样作用域化。JDK 25 主要使作用域规则更一致、更精确(如移除含糊边界),让编译器校验"模式变量在使用点确实已匹配"。

模式变量作用域由"流经分析"决定(匹配成功才可用),JDK 25 细化规则使 instanceof 与 switch 模式变量的作用域一致且精确,提升类型安全。

if (obj instanceof String s) {   // s 仅在 true 分支作用域内可用
    System.out.println(s.length());
}
// 流经分析:s 只在匹配后可用
switch (obj) {
    case String s -> System.out.println(s.length());  // s 在 case 内可用
    default -> {}
}
#

93. Object 通用方法(equals/hashCode/toString/clone/finalize/getClass/wait/notify)的正确重写

Object 通用方法(equals/hashCode/toString/clone/finalize/getClass/wait/notify)如何正确重写?

  • equals/hashCode 契约
  • toString/clone/finalize
  • wait/notify/getClass

Object 通用方法:equals 重写需满足自反/对称/传递/一致/非空性,且与 hashCode 一致(equals 相等则 hashCode 相等);hashCode 重写需与 equals 一致,用不可变字段组装;toString 提供可读表示(默认输出类名+hashCode),建议呈现关键字段;clone 需实现 Cloneable 且注意浅/深拷贝,多数场景不用;finalize 已废弃(JDK 9+,用 try-with-resources/Cleaner 替代);getClass 返回运行时类,不能重写(final);wait/notify/notifyAll 用于对象监视器(需在 synchronized 内,不能重写这些方法用于别的用途)。正确重写重点是 equals/hashCode 契约、toString 可读、clone 谨慎、avoid finalize。

equals/hashCode 是核心契约(影响集合),toString 服务调试,clone/finalize 尽量不用,wait/notify 是监视器原语。正确重写维护对象语义与集合正确性。

public final class Person {
    private final String name;
    private final int age;
    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person p)) return false;
        return age == p.age && name.equals(p.name);
    }
    @Override public int hashCode() { return Objects.hash(name, age); }
    @Override public String toString() { return "Person(" + name + "," + age + ")"; }
}
#

94. final、finally、finalize 三者的本质差异

final、finally、finalize 三者的本质差异是什么?

  • final 关键字
  • finally 块
  • finalize 方法

final 是关键字,修饰类(不可继承)、方法(不可覆写)、变量(不可变)、参数(不可改);finally 是 try/catch 的块,无论是否异常都执行,用于清理;finalize 是 Object 的方法,在 GC 回收对象前调用,用于清理资源,但已废弃(JDK 9+,不可靠、有性能与安全风险,用 try-with-resources/Cleaner 替代)。三者除拼写相近外本质无关:final 是修饰符,finally 是异常处理结构,finalize 是对象生命周期钩子。工程上:用 final 保证不可变/不可继承,用 finally/try-with-resources 保证清理,避免用 finalize。

final/finally/finalize 是"老生常谈"的三大概念,本质完全不同:修饰符/异常处理/生命周期方法,finalize 已废弃。

final class C {}            // final: 不可继承
final int X = 1;            // final: 不可变
try { ... } finally { ... } // finally: 必执行
// finalize() 已废弃,用 try-with-resources / Cleaner 替代
#

95. switch 的 fall-through 历史陷阱与箭头语法(->)的 break 语义差异

switch 的 fall-through 历史陷阱是什么?与箭头语法(->)的 break 语义有何差异?

  • switch 语句的 fall-through
  • 箭头语法无 fall-through
  • break 语义差异

传统 switch 语句(case X: 后跟语句)默认"fall-through":匹配一个 case 后,若没有 break/return,会继续执行后续 case 的语句,这常导致意外执行多个 case(历史陷阱)。箭头语法(case X -> 语句)在 Java 14 引入:每个 case 分支不 fall-through,执行完自动结束该分支,无需 break,且可作为表达式返回值。差异:传统 switch 需 break 防止 fall-through,箭头语法自带终止(无 fall-through、无 break);箭头语法下 null 处理与多标签(case 1,2 ->)不同。箭头语法更安全、更简洁,避免 fall-through 陷阱。

fall-through 是传统 switch 的经典陷阱(漏 break 导致连锁执行),箭头语法用"自动终止"消除该陷阱,语义更安全。

int x = 2;
switch (x) { case 1: System.out.println("1"); case 2: System.out.println("2"); }
// 传统:输出 "2"(无 break 会 fall-through,但此处 x=2 只匹配 case2)
switch (x) {
    case 1 -> System.out.println("1");
    case 2 -> System.out.println("2");   // 箭头:无 fall-through,执行完即结束
}
#

96. try-with-resources 在 JDK 25 中对 Scoped Values 与清理钩子的支持边界

try-with-resources 在 JDK 25 中对 Scoped Values 与清理钩子的支持边界是什么?

  • Scoped Values(JEP 506)
  • try-with-resources 作为作用域
  • 清理钩子支持

Scoped Values(JEP 506,JDK 25 终版)是一种线程局部式数据传递机制,比 ThreadLocal 更安全(不可变、绑定作用域、可继承给虚拟线程)。Scoped Values 使用 try-with-resources 形式:try (var scope = ScopedValue.where(KEY, value).run(() -> {...})) 或 try (var ctx = ScopedValue.where(...)) 绑定值,在 try 块内通过 KEY.get() 读取,try 结束后作用域值自动失效(类似清理钩子)。支持边界:ScopedValue 必须在 try-with-resources 作用域内绑定,try 结束(无论正常/异常)自动清理,不支持跨线程手动设置(除非显式调用)。try-with-resources 作为 Scoped Values 的作用域载体,保证绑定值的生命周期与清理。

Scoped Values 用 try-with-resources 定义值的作用域,结束自动清理,是 ThreadLocal 的安全替代,Try-With 保证了绑定与清理的配对。

static final ScopedValue<String> USER = ScopedValue.newInstance();
try (var scope = ScopedValue.where(USER, "alice")) {
    String u = USER.get();   // 作用域内读取绑定值
    runTask();               // 子任务可见
}   // try 结束自动清理绑定值
#

97. var 局部类型推断在 JDK 25/26 中对涉及记录类与密封类的复杂泛型推断边界如何演化

var 局部类型推断在 JDK 25/26 中对涉及记录类与密封类的复杂泛型推断边界如何演化?

  • var 与泛型推断
  • record 与密封类的推断
  • 推断边界演化

var 局部类型推断从初始化器推断类型,对涉及 record 与密封类的复杂泛型场景,推断边界持续演化:JDK 25/26 中 var 能更准确地推断泛型类型(如 record 构造器返回的泛型、嵌套泛型),并利用类型推断改进(如与模式匹配、instanceof 结合时的类型细化)。边界:var 仍是静态推断,推断出的是初始化器的静态类型(不会因密封类而"收窄"为具体子类型,除非编译器 able 推断);对 record 的构造器泛型参数、密封类 switch 的分支类型,var 推断与显式类型声明需注意作用域。演化方向是让推断更精确、与模式匹配/密封类协作更好,但 var 不改变静态类型语义。

var 的推断依赖初始化器静态类型,record/密封类场景下推断仍遵守静态类型规则,JDK 25/26 细化使推断更精确但不改变静态类型语义。

record Box<T>(T value) {}
var box = new Box<Long>(1L);   // 推断为 Box<Long>
var shapes = List.of(new Circle(1), new Square(2));  // 推断为 List<Shape>(密封子类共同类型)
// var 仍是静态类型,不因密封类收窄为具体子类型
#

98. 异常性能基准测试的常见误区

异常性能基准测试的常见误区有哪些?

  • 基准测试误区
  • 异常开销的测量
  • JIT 与逃逸分析

异常性能基准测试常见误区:1)未预热 JIT,测得解释执行或未优化的开销,与真实运行不符;2)未考虑 JIT 优化(如异常路径被优化、try 块零成本),误以为 try/catch 本身昂贵;3)把异常创建(fillInStackTrace 栈遍历)与异常传播混为一谈,未区分成本来源;4)用简单循环无法复现真实栈深度(栈深影响 fillInStackTrace 成本);5)忽略 GC 影响(异常对象分配);6)基准测试环境(CPU、JVM 参数)与生产差异。正确做法:用 JMH 预热、正确设置编译选项、控制栈深、区分"创建异常"与"抛异常"成本、测量真实分配。避免被"异常很慢"的陈旧结论误导。

异常性能误区源于未正确测量:JIT 优化、栈深、异常创建与传播分离、GC 分配。用 JMH 预热并控制变量才能得到可靠结论。

// JMH 基准示例(预热 + 正确测量)
@Benchmark @Fork(1) @Warmup(iterations=5) @Measurement(iterations=5)
public boolean throwException() {
    try { risky(); } catch (Exception e) { return false; }  // 异常路径
}
#

99. record 的紧凑构造器(Compact Constructor)与规范构造器在参数校验与防御性拷贝上的分工

record 的紧凑构造器(Compact Constructor)与规范构造器在参数校验与防御性拷贝上如何分工?

  • record 紧凑构造器(compact constructor)
  • 规范构造器(canonical constructor)
  • 参数校验与防御性拷贝

record 的规范构造器(canonical constructor)是组件参数恰好与组件一致的构造器;紧凑构造器(compact constructor)是规范构造器的简写,不重复参数列表(隐式参数),在构造器体内可对参数校验、做防御性拷贝,然后"隐式地"把参数赋给组件(编译器自动完成赋值)。分工:参数校验(如 null 检查、范围检查)在紧凑构造器/规范构造器中进行;防御性拷贝(如拷贝可变组件)在构造器中完成,确保组件不可变。紧凑构造器体内可修改参数(如对可变参数做 copy)再赋值,规范构造器则需显式赋值。反序列化时 record 不走构造器(按组件名直接赋值),因此紧凑构造器的校验在反序列化时不执行(有专门的反序列化机制)。

紧凑构造器是规范构造器的简化,用于校验与防御性拷贝;record 反序列化绕过构造器,故校验需另行考虑。

record Range(int start, int end) {
    Range {   // 紧凑构造器:校验 + 防御性拷贝
        if (start > end) throw new IllegalArgumentException("start>end");
        // 隐式赋值 this.start=start; this.end=end;
    }
}