日期、字符串、I/O 与序列化

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

1. BIO 的 one-connection-per-thread 模型在 C10K 问题下的资源瓶颈与伪异步 I/O 局限

BIO 的 one-connection-per-thread 模型在 C10K 问题下的资源瓶颈与伪异步 I/O 局限是什么?

  • BIO 线程模型
  • C10K 瓶颈
  • 伪异步 I/O

BIO(Blocking I/O)的 one-connection-per-thread 模型:每个连接分配一个线程,线程阻塞在读写。C10K(1 万连接)问题下资源瓶颈:1)线程数随连接数线性增长,1 万连接需 1 万线程,线程创建/切换/栈内存(默认 1MB 栈)开销巨大,内存耗尽;2)大量线程阻塞等待,多数线程空闲,浪费资源;3)线程上下文切换开销大。伪异步 I/O(线程池 + 阻塞连接):用有界线程池替代每连接一线程,线程池复用线程,但仍是阻塞 I/O,线程池大小受限,连接多时线程池排队/拒绝,吞吐受限。伪异步的局限:线程池固定大小,阻塞连接占线程,连接数超过线程池时无法并发处理,本质仍是阻塞。

BIO 每连接一线程,C10K 下线程数爆炸浪费资源;伪异步用线程池复用但仍是阻塞,线程池有限连接多时受限。

// BIO:每连接一线程(C10K 下线程爆炸)
ExecutorService pool = Executors.newFixedThreadPool(100);   // 伪异步:有界线程池
while (true) {
    Socket s = server.accept();
    pool.submit(() -> handle(s));   // 阻塞读写,线程池有限
}
#
★★★

2. BIO、NIO、AIO 三种 I/O 模型的本质差异与适用场景辨析

BIO、NIO、AIO 三种 I/O 模型的本质差异与适用场景是什么?

  • BIO/NIO/AIO
  • 阻塞/非阻塞
  • 适用场景

BIO(Blocking I/O):阻塞式,读写阻塞线程,每连接一线程,简单但并发受限;NIO(Non-blocking I/O):非阻塞 + Selector 多路复用,一个线程处理多个连接(注册事件、事件驱动),高并发,适合大量连接;AIO(Asynchronous I/O):异步非阻塞,完成回调(CompletionHandler)或 Future,读写完成才通知,适合大量并发 I/O 且需异步回调。本质差异:BIO 阻塞(线程阻塞等待)、NIO 非阻塞+多路复用(线程轮询事件)、AIO 异步(完成回调)。适用:BIO 连接少、简单;NIO 高并发连接(HTTP 服务器、网关);AIO 异步 I/O 密集(文件、网络,但 Java 的 AIO 生态不成熟,多软件用 NIO 的 Netty 实现异步)。

BIO 阻塞、NIO 非阻塞+多路复用、AIO 异步回调;BIO 简单连接少、NIO 高并发、AIO 异步 I/O。

// NIO:Selector 多路复用
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ);
// AIO:完成回调
AsynchronousFileChannel channel = AsynchronousFileChannel.open(...);
channel.read(buf, 0, null, new CompletionHandler<>() { ... });
#
★★★

3. Date/Calendar 的设计缺陷与线程安全问题

Date/Calendar 的设计缺陷与线程安全问题是什么?

  • Date/Calendar 缺陷
  • 可变性
  • 线程安全

Date/Calendar 的设计缺陷:1)可变性:Date/Calendar 可变(setTime 等),易被修改引发状态不一致;2)索引混乱:Calendar 月份从 0 开始(1 月=0),易错;3)线程不安全:SimpleDateFormat 非线程安全(Calendar 内部状态),多线程共享会错乱;Date 本身可变也非线程安全;4)时区处理笨拙:Calendar 时区/偏移处理复杂;5)API 混乱、不直观。缺陷根源:可变对象 + 不良设计。替代:java.time(JSR-310,JDK 8+)不可变、线程安全、清晰。SimpleDateFormat 线程不安全需用 DateTimeFormatter(不可变)或 ThreadLocal。

Date/Calendar 可变、月份 0 索引、SimpleDateFormat 线程不安全,用 java.time 不可变 API 替代。

// Calendar 月份 0 开始
Calendar c = Calendar.getInstance();
c.set(Calendar.MONTH, 0);   // 1 月
// SimpleDateFormat 线程不安全
// 用 DateTimeFormatter(不可变)
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd");
#
★★★

4. DateTimeFormatter 的线程安全与缓存策略

DateTimeFormatter 的线程安全与缓存策略是什么?

  • DateTimeFormatter 线程安全
  • 缓存策略
  • 使用

DateTimeFormatter 是不可变、线程安全的(JDK 8+),可被多个线程共享(与 SimpleDateFormat 不同)。缓存策略:由于线程安全,可把 DateTimeFormatter 定义为 static final 常量缓存复用,避免重复创建(创建成本高)。预设格式(ISO_LOCAL_DATE 等)也可用。缓存:用 static final 缓存常用格式,或缓存预编译的 DateTimeFormatter(对象不可变无需包装)。注意:DateTimeFormatter 不可变安全,但若配合 Locale/时区注意字段。缓存策略:static final 复用,避免每次创建,提升性能。

DateTimeFormatter 不可变线程安全,可 static final 缓存复用,避免重复创建,这是它优于 SimpleDateFormat 的关键。

public static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 线程安全,可共享
String s = date.format(DATE_FMT);
#
★★★

5. FileChannel.transferTo 零拷贝的底层 sendfile/splice 实现与适用边界

FileChannel.transferTo 零拷贝的底层 sendfile/splice 实现与适用边界是什么?

  • transferTo 零拷贝
  • sendfile/splice
  • 适用边界

FileChannel.transferTo 实现零拷贝:把文件数据直接传输到目标(如 SocketChannel),避免"用户态-内核态"多次拷贝。底层:Linux 用 sendfile()(/splice)系统调用,数据在内核态直接拷贝(文件→socket),不经过用户态缓冲区,减少 CPU 拷贝与上下文切换。适用边界:transferTo 适合"文件→网络/socket"的传输(零拷贝),但:1)目标必须是支持零拷贝的通道(SocketChannel,非普通文件所有场景);2)sendfile 适合"文件到 socket",某些场景(socket 到 socket)用 splice;3)平台相关(Linux 支持,Windows 差异);4)transferTo 返回传输字节数,可能需循环;5)不适合需要处理/修改数据的场景(零拷贝只读传)。适用:大文件静态传输(HTTP 文件服务、静态资源)用零拷贝。

transferTo 用 sendfile/splice 内核态零拷贝,适合文件→socket 大文件传输,减少拷贝,但平台相关、只读传。

// 零拷贝:文件直接传输到 socket channel
try (FileChannel fc = FileChannel.open(path); SocketChannel sc = ...) {
    fc.transferTo(0, fc.size(), sc);   // 内核态零拷贝
}
#
★★★

6. Files/Path(NIO.2)相对传统 File 类的改进与工程价值

Files/Path(NIO.2)相对传统 File 类的改进与工程价值是什么?

  • NIO.2 Path/Files
  • 相对 File 改进
  • 工程价值

NIO.2 的 Path/Files(JDK 7+)相对传统 File 类改进:1)Path 是路径抽象(不可变、可组合),Files 提供丰富静态方法(读取、写入、遍历、复制、移动);2)Files.readAllLines/write/lines 流式读写,简洁;3)Files.walk/list 遍历目录;4)Files.lines 惰性读大文件;5)异常处理(IOException 明确);6)支持符号链接、属性、WatchService(文件监听)。工程价值:更简洁、功能更全、流式/惰性、可扩展,替代 File 的操作。File 类功能弱、API 混乱。Files/Path 是现代文件操作标准。

NIO.2 的 Path/Files 提供简洁静态方法、流式读写、目录遍历、监听,功能全面,替代传统 File 类。

Path p = Path.of("data.txt");
List<String> lines = Files.readAllLines(p);           // 读全部
Files.write(p, "hello".getBytes());                   // 写
try (Stream<String> s = Files.lines(p)) { ... }       // 惰性读
Files.walk(dir).filter(Files::isRegularFile).forEach(...);  // 遍历
#
★★★

7. Instant 在跨时区持久化的最佳实践

Instant 在跨时区持久化的最佳实践是什么?

  • Instant 时间戳
  • 跨时区持久化
  • UTC 存储

Instant 表示"时间线上的瞬间"(UTC 时间戳,独立于时区),跨时区持久化最佳实践:1)用 Instant(或 UTC 时间戳)存储时间点,避免时区偏移(Instant 是 UTC 基准,不随本地时区变化);2)持久化用 UTC 明确(如 ISO 8601 带 Z 的 UTC 表示,或 epoch 秒/毫秒);3)本地时间(LocalDateTime)不存时区,会导致跨时区歧义;4)展示时再转换为本地时区(ZonedDateTime 按用户时区)。最佳实践:存储用 Instant/UTC,传输用 UTC ISO 8601,展示用用户时区。避免存储 LocalDateTime(无时区)导致跨时区错误。

Instant 是 UTC 时间戳独立于时区,跨时区持久化用 Instant/UTC 存储,展示再转本地时区,避免 LocalDateTime 歧义。

// 存储:Instant(UTC)
Instant now = Instant.now();   // 无时区
// 持久化:UTC ISO 8601 或 epoch
String iso = now.toString();   // 2026-08-03T00:00:00Z
// 展示:转用户时区
ZonedDateTime z = now.atZone(ZoneId.of("Asia/Shanghai"));
#
★★★

8. NIO.2 的 Files.walk/Files.list 流式遍历在大目录下的资源泄漏风险与 Spliterator 控制

NIO.2 的 Files.walk/Files.list 流式遍历在大目录下的资源泄漏风险与 Spliterator 控制是什么?

  • Files.walk/list 流
  • 资源泄漏
  • Spliterator 控制

Files.walk 与 Files.list 返回 Stream(绑定目录流),必须关闭(try-with-resources),否则目录句柄泄漏(打开的文件句柄不释放)。大目录下资源泄漏风险:不关闭流导致句柄泄漏,long 占用系统资源,最终句柄耗尽。Spliterator 控制:Files.walk 返回的流有 Spliterator,可控制遍历(如 limit 提前停止、并行遍历),但需注意:1)流绑定目录,未关闭句柄泄漏;2)大目录遍历应 try-with-resources 关闭;3)limit 等短路可减少遍历但需关闭。正确:try (Stream s = Files.walk(dir)) { ... } 确保关闭;大目录用流式逐步处理而非全量收集。

Files.walk/list 返回绑定目录的流,需 try-with-resources 关闭避免句柄泄漏,大目录流式处理并用 Spliterator 控制。

try (Stream<Path> paths = Files.walk(dir)) {   // 必须关闭
    paths.filter(Files::isRegularFile).limit(100).forEach(...);
}   // 自动关闭,避免句柄泄漏
#
★★★

9. NumberFormat/DateFormat 的本地化与线程安全

NumberFormat/DateFormat 的本地化与线程安全是什么?

  • NumberFormat/DateFormat
  • 本地化
  • 线程安全

NumberFormat/DateFormat(java.text)用于格式化数字/日期,本地化:通过 Locale 获得本地化的格式(getNumberInstance(Locale)、getDateInstance(Locale)),显示本地化的数字/日期格式。线程安全:NumberFormat/DateFormat 非线程安全(内部有可变状态,如 Calendar、字段),多线程共享会错乱,需 ThreadLocal 或每次创建或同步。替代:数字/日期格式化用 java.time.DateTimeFormatter(线程安全)或 DecimalFormat(也非线程安全需注意)。本地化:NumberFormat 按 Locale 格式化数字(千分位、符号),DateFormat 按 Locale 格式化日期。线程不安全需隔离。

NumberFormat/DateFormat 按 Locale 本地化,但非线程安全(可变状态),需 ThreadLocal/每次创建,或用线程安全替代。

// 本地化
NumberFormat nf = NumberFormat.getNumberInstance(Locale.CHINA);
// 线程不安全:用 ThreadLocal 隔离
ThreadLocal<NumberFormat> tl = ThreadLocal.withInitial(() -> NumberFormat.getNumberInstance());
#
★★★

10. Pattern 的预编译与 flags 优化

Pattern 的预编译与 flags 优化是什么?

  • Pattern 预编译
  • flags 优化
  • 缓存

Pattern 预编译:正则表达式编译为 Pattern 对象(Pattern.compile)是一次性开销(解析、编译为内部表示),应预编译并复用(static final),避免每次匹配都重新编译(编译成本高)。flags 优化:Pattern.compile(pattern, flags) 用 flags 提升性能或语义:CASE_INSENSITIVE(忽略大小写)、MULTILINE、DOTALL、UNICODE_CASE 等。注意 flags 影响匹配语义与性能(如 CASE_INSENSITIVE 有开销)。优化:缓存 Pattern(static final 或 Pattern 缓存),避免重复编译;用预编译 Pattern 的 matcher 匹配。Matcher 可复用(reset)。flags 按需选择。

Pattern 预编译避免重复编译(static final 缓存),flags 控制匹配语义,预编译复用 Pattern 是性能优化。

private static final Pattern P = Pattern.compile("\\d+", Pattern.CASE_INSENSITIVE);
// 复用 Pattern
Matcher m = P.matcher(input);
if (m.find()) { ... }
#
★★★

11. Pattern/Matcher 的线程安全与回溯陷阱

Pattern/Matcher 的线程安全与回溯陷阱是什么?

  • Pattern 线程安全
  • Matcher 线程不安全
  • 回溯

Pattern 是线程安全、不可变的,可被多个线程共享使用;Matcher 非线程安全(内部有状态:匹配位置、组),不能多线程共享,需每次 matcher() 创建或 ThreadLocal。回溯陷阱:正则的贪婪量词(.、a+)在匹配失败时回溯(backtracking),某些模式(嵌套量词、..*)可能指数级回溯,导致 ReDoS(性能 DoS)。避免:用非贪婪量词、原子组((?>...))、避免嵌套量词、限制输入长度、用更简单的正则。回溯陷阱是性能/安全风险,需注意正则复杂度。

Pattern 线程安全可共享,Matcher 非线程安全需每次创建;正则回溯(嵌套量词)可能指数级导致 ReDoS,需避免。

// Pattern 可共享,Matcher 每次创建
Matcher m = P.matcher(input);   // 非线程安全,每次新建
// 回溯陷阱:.*.* 嵌套量词可能指数回溯
// 避免:(non-greedy) 或原子组 (?>...)
#
★★★

12. Pipe(SourceChannel/SinkChannel)的线程间通信语义与典型坑点

Pipe(SourceChannel/SinkChannel)的线程间通信语义与典型坑点是什么?

  • Pipe
  • SourceChannel/SinkChannel
  • 线程间通信与坑点

java.nio.channels.Pipe 是 NIO 的线程间通信管道:一个线程写 SinkChannel(写入端),另一线程读 SourceChannel(读取端),实现线程间数据传递。语义:一对一的管道(一个写端一个读端),数据从一个线程流向另一个,类似 FIFO 队列。典型坑点:1)阻塞:若写端写满 / 读端读空,阻塞(需配合 Selector 非阻塞或合理容量);2)不是线程安全的双向:Pipe 是单向(一写一读),双向需两个 Pipe;3)容量:Pipe 有缓冲容量,写满阻塞;4)关闭:需正确关闭两端,否则资源泄漏/阻塞;5)与 Selector 结合:SourceChannel 可注册 Selector 读取。Pipe 用于线程间传递数据(如测试、日志)。

Pipe 是单向线程间通道(Sink 写、Source 读),有容量/阻塞/单向坑点,需正确关闭与 Selector 结合。

Pipe pipe = Pipe.open();
Pipe.SinkChannel sink = pipe.sink();       // 写端
Pipe.SourceChannel source = pipe.source(); // 读端
// 线程 A 写 sink,线程 B 读 source
#
★★★

13. String.intern 返回的引用在 JDK 6 与 JDK 7+ 中分别存储在永久代还是堆中,这一变化对内存与回收有何影响

String.intern 返回的引用在 JDK 6 与 JDK 7+ 中分别存储在永久代还是堆中?这一变化对内存与回收有何影响?

  • String.intern
  • 永久代 vs 堆
  • 内存与回收

String.intern 返回的字符串引用(字符串常量池)在 JDK 6 存储在永久代(PermGen),JDK 7+ 存储在堆(Heap,StringTable 在堆)。变化影响:1)JDK 6 永久代大小固定,intern 过多导致永久代 OOM(PermGen OOM);2)JDK 7+ 永久代移到堆,intern 字符串在堆中,受堆大小限制,堆可 GC(intern 字符串无引用时回收);3)JDK 7 起 intern 的字符串对象是堆中的对象,可被 GC 回收(无强引用时),而 JDK 6 永久代字符串随永久代回收(不常回收)。影响:JDK 7+ intern 更安全(堆大、可回收),但 intern 仍应谨慎(StringTable 固定大小,大量 unique 字符串拖慢查找、内存压力)。JDK 7 起的 StringTable 大小可调(-XX:StringTableSize)。

intern 字符串 JDK 6 在永久代(固定易 OOM),JDK 7+ 在堆(可回收、受堆限制),intern 仍应谨慎。

// JDK 6:intern 在永久代,易 PermGen OOM
// JDK 7+:intern 在堆,可回收
String s = "abc".intern();   // 存入字符串常量池
// 大量 intern 拖慢 StringTable 查找,可调大 -XX:StringTableSize
#
★★★

14. String、StringBuilder、StringBuffer 的差异与适用场景

String、StringBuilder、StringBuffer 的差异与适用场景是什么?

  • String 不可变
  • StringBuilder 可变非线程安全
  • StringBuffer 可变线程安全

String 不可变(final、内容不可变),拼接产生新对象,适合常量/不可变字符串;StringBuilder 可变、非线程安全,append 高效,适合单线程字符串拼接(推荐);StringBuffer 可变、线程安全(方法 synchronized),并发安全但性能差(全方法同步),适合多线程共享拼接(但通常用 StringBuilder + 同步更好)。差异:不可变 vs 可变;线程安全 vs 非线程安全。适用:字符串常量用 String,单线程拼接用 StringBuilder,多线程共享拼接(罕见)用 StringBuffer。JDK 8+/StringConcatFactory 优化 + 拼接,但循环拼接用 StringBuilder。

String 不可变、StringBuilder 可变高效非线程安全、StringBuffer 可变线程安全(性能差),单线程拼接用 StringBuilder。

String s = "a";   // 不可变
StringBuilder sb = new StringBuilder();   // 单线程拼接,高效
sb.append("a").append("b");
StringBuffer sbf = new StringBuffer();    // 线程安全,性能差
sbf.append("a");
#
★★★

15. serialPersistentFields 如何显式声明参与序列化的字段集合,与 transient 及字段重命名场景的兼容策略如何配合

serialPersistentFields 如何显式声明参与序列化的字段集合?与 transient 及字段重命名场景的兼容策略如何配合?

  • serialPersistentFields
  • transient
  • 字段重命名兼容

serialPersistentFields(ObjectStreamClass)是静态字段,显式声明"参与序列化的字段集合",声明外的字段不序列化(即使非 transient)。语法:private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("name", String.class), ... };。与 transient 配合:transient 字段不序列化(跳过),serialPersistentFields 声明的字段才序列化(比 transient 更显式)。字段重命名兼容:通过 serialPersistentFields 声明"旧字段名"或 readObject/writeObject 自定义,实现字段重命名/迁移兼容(如字段改名但序列化流保持旧名,或用 readObject 适配)。serialPersistentFields 用于显式控制字段集合与版本兼容。

serialPersistentFields 显式声明序列化字段集合,transient 跳过字段,配合 readObject/writeObject 实现字段重命名与版本兼容。

private static final ObjectStreamField[] serialPersistentFields = {
    new ObjectStreamField("name", String.class),
    new ObjectStreamField("age", int.class)
};   // 只序列化这些字段
// transient 字段不序列化
// readObject/writeObject 实现字段重命名兼容
#
★★

16. AIO(AsynchronousFileChannel)的 CompletionHandler 回调模型与 Future 模式的取舍

AIO(AsynchronousFileChannel)的 CompletionHandler 回调模型与 Future 模式的取舍是什么?

  • AsynchronousFileChannel
  • CompletionHandler 回调
  • Future 模式

AsynchronousFileChannel(AIO)提供异步 I/O,两种完成方式:1)CompletionHandler 回调:传入回调对象,操作完成时回调(completed/failed),事件驱动,需注意回调线程与回调内错误处理;2)Future 模式:操作返回 Future,调用 get() 阻塞等待或轮询,更命令式。取舍:CompletionHandler 回调适合"异步、非阻塞、事件驱动"(不阻塞主线程,但回调逻辑复杂、嵌套回调、线程管理);Future 模式适合"需要结果/阻塞等待"(简单,但 get() 阻塞)。现代 Java 常结合 CompletableFuture 包装异步操作。选择:纯异步回调用 CompletionHandler,需要结果/阻塞用 Future,或包装 CompletableFuture。

CompletionHandler 回调适合事件驱动异步(逻辑复杂),Future 模式 get() 阻塞(简单),可包装 CompletableFuture。

// CompletionHandler 回调
channel.read(buf, 0, null, new CompletionHandler<Integer, Void>() {
    public void completed(Integer r, Void a) { ... }
    public void failed(Throwable e, Void a) { ... }
});
// Future 模式
Future<Integer> f = channel.read(buf, 0, null);
f.get();   // 阻塞或轮询
#
★★

17. ByteBuffer 字节序(ByteOrder.BIG_ENDIAN/LITTLE_ENDIAN)在网络协议与文件格式解析中的影响

ByteBuffer 字节序(ByteOrder.BIG_ENDIAN/LITTLE_ENDIAN)在网络协议与文件格式解析中的影响是什么?

  • ByteOrder
  • BIG_ENDIAN/LITTLE_ENDIAN
  • 网络协议/文件格式

ByteBuffer 的字节序(ByteOrder)决定多字节数据(int、long)的字节排列:BIG_ENDIAN(大端,高位在前)与 LITTLE_ENDIAN(小端,低位在前)。网络协议与文件格式解析中的影响:1)网络协议(TCP/IP)用大端(BIG_ENDIAN,网络字节序),解析网络包必须用 BIG_ENDIAN 读取,否则数据错乱;2)文件格式(如某些二进制格式)可能用小端(LITTLE_ENDIAN),需按格式声明;3)Java ByteBuffer 默认 BIG_ENDIAN,需按格式设置 order(ByteOrder)。解析时字节序不匹配会导致多字节值错误。正确:按网络协议/文件格式规范设置 ByteOrder,再读多字节。

网络协议用大端(BIG_ENDIAN),文件格式可能小端,ByteBuffer 需按格式设置 order 否则多字节值解析错误。

ByteBuffer buf = ByteBuffer.allocate(4);
buf.order(ByteOrder.BIG_ENDIAN);     // 网络字节序(大端)
int v = buf.getInt();                // 按大端读
buf.order(ByteOrder.LITTLE_ENDIAN);  // 小端文件格式
#
★★

18. ByteBuffer 的 position/limit/capacity 三指针与 flip/rewind/clear/compact 的状态机语义

ByteBuffer 的 position/limit/capacity 三指针与 flip/rewind/clear/compact 的状态机语义是什么?

  • position/limit/capacity
  • flip/rewind/clear/compact
  • 状态机

ByteBuffer 三指针:position(当前读写位置)、limit(可读写边界)、capacity(容量)。状态机语义:flip():从写转读——limit=position,position=0(读已写入的数据);rewind():重新读——position=0(保留 limit,重复读);clear():清空——position=0,limit=capacity(准备重新写);compact():压缩——把未读数据移到开头,position=未读长度,limit=capacity(准备继续写,保留未读)。应用:写后 flip 转读,读后 clear 或 compact 继续写。理解三指针与四个方法的状态转换是正确使用 ByteBuffer 的关键。

flip 写转读、rewind 重复读、clear 清空重写、compact 保留未读继续写,三指针状态转换是 ByteBuffer 核心。

ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put("hello".getBytes());   // 写
buf.flip();                    // 写转读:limit=position, position=0
byte[] d = new byte[buf.remaining()];
buf.get(d);                    // 读
buf.clear();                   // 清空重写
#
★★

19. Channel 与 Selector 多路复用机制在 Linux epoll 与 macOS kqueue 上的实现差异

Channel 与 Selector 多路复用机制在 Linux epoll 与 macOS kqueue 上的实现差异是什么?

  • Selector 多路复用
  • epoll/kqueue
  • 平台差异

Selector 多路复用:一个线程通过 Selector 同时监听多个 Channel(注册事件),事件就绪时处理,避免每连接一线程。底层实现平台差异:Linux 用 epoll(事件驱动,注册 fd 后等待事件,可扩展、高效),macOS 用 kqueue(事件队列,类似 epoll),Windows 用不同机制。差异:1)epoll 与 kqueue 都是"事件驱动"(注册 fd 监听就绪),比 select/poll 高效(O(1) 事件、无 fd 数限制);2)epoll 是 Linux 专用系统调用,kqueue 是 BSD/macOS;3)Java NIO Selector 封装底层(Linux 用 epoll,macOS 用 kqueue),Java API 统一,但底层实现不同影响性能与行为;4)epoll 有"空轮询 bug"(曾需重建 Selector),kqueue 无此问题。Java 的 Selector 实现用各平台的最优机制。

Selector 底层 Linux 用 epoll、macOS 用 kqueue(都是事件驱动高效),Java 封装统一,但底层差异影响性能与行为。

// Java NIO Selector(底层平台相关)
Selector selector = Selector.open();   // Linux: epoll, macOS: kqueue
channel.register(selector, SelectionKey.OP_READ);
selector.select();   // 阻塞等待事件就绪
#
★★

20. DirectByteBuffer 的分配、回收(Cleaner/PhantomReference)与 -XX:MaxDirectMemorySize 约束

DirectByteBuffer 的分配、回收(Cleaner/PhantomReference)与 -XX:MaxDirectMemorySize 约束是什么?

  • DirectByteBuffer
  • 直接内存
  • Cleaner 回收

DirectByteBuffer(直接内存,堆外)通过 ByteBuffer.allocateDirect 分配,直接内存不占堆,用于零拷贝/网络 I/O(避免堆拷贝)。回收:DirectByteBuffer 用 Cleaner(PhantomReference 关联)在 GC 时回收直接内存——当 DirectByteBuffer 对象被 GC,其 Cleaner 触发 free 直接内存。但直接内存回收依赖 GC(不是立即),且不受堆 GC 管理。约束:-XX:MaxDirectMemorySize 限制直接内存总量(默认约等于堆大小),分配超限抛 OutOfMemoryError(Direct buffer memory)。注意:直接内存泄漏需及时释放(ByteBuffer 引用释放后 GC 回收),用 Cleaner 或显式释放。

DirectByteBuffer 用 Cleaner(PhantomReference)在 GC 时回收直接内存,-XX:MaxDirectMemorySize 限制总量,超限 OOM。

ByteBuffer buf = ByteBuffer.allocateDirect(1024);   // 直接内存(堆外)
// 回收:Cleaner 关联 PhantomReference,GC 时 free 直接内存
// -XX:MaxDirectMemorySize=1g 限制直接内存
#
★★

21. FileChannel.lock 文件锁的进程级语义与 JVM 内多线程共享同一 Channel 的边界

FileChannel.lock 文件锁的进程级语义与 JVM 内多线程共享同一 Channel 的边界是什么?

  • FileChannel.lock
  • 进程级锁
  • 多线程共享边界

FileChannel.lock() 获取文件锁(FileLock),是"进程级"锁:锁跨越进程(不同 JVM 进程互斥),JVM 内多个线程通过同一 Channel 锁同一区域时,锁是共享的(同一 JVM 内不互斥,因为锁以进程为单位)。边界:1)进程级:不同进程互斥,同一 JVM 内多线程共享同一 Channel 的锁不互斥(JVM 内互斥需用 JVM 内锁);2)lock 阻塞获取,tryLock 非阻塞;3)锁是排他(overlapping)或共享;4)锁与 Channel 生命周期关联,Channel 关闭释放锁;5)跨平台行为差异(Windows/Unix)。FileLock 用于跨进程文件互斥(如集群唯一性),JVM 内多线程不互斥需注意。

FileChannel.lock 是进程级锁(跨进程互斥),JVM 内多线程共享同一 Channel 不互斥,JVM 内互斥需其他锁。

try (FileChannel fc = FileChannel.open(path, StandardOpenOption.WRITE)) {
    FileLock lock = fc.lock();   // 进程级阻塞锁
    // 跨进程互斥
    lock.release();
}
#
★★

22. MappedByteBuffer 内存映射文件的页缓存机制与大文件随机读写性能

MappedByteBuffer 内存映射文件的页缓存机制与大文件随机读写性能是什么?

  • MappedByteBuffer
  • 页缓存
  • 大文件随机读写

MappedByteBuffer(内存映射文件)通过 FileChannel.map 把文件映射到内存地址空间,读写文件像操作内存。页缓存机制:映射后文件内容由 OS 页缓存管理,读写通过页缓存(mmap),无需显式 read/write 系统调用(按需加载页),大文件随机读写性能:1)随机访问按页映射,命中页缓存则快速;2)避免频繁系统调用(直接指针访问);3)大文件共享映射(多个进程/线程共享页缓存)。性能:随机读写大文件用映射可提升(页缓存 + 指针访问),但需注意:1)映射大小受虚拟地址空间限制;2)映射文件修改需 force() 落盘;3)映射与文件大小变化需重新映射。适用于大文件随机读写、共享内存。

MappedByteBuffer 用 mmap 页缓存映射文件,随机读写像内存(避免系统调用),需 force() 落盘、注意映射大小。

MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, size);
mbb.putInt(position, value);   // 随机读写像内存
mbb.force();                   // 落盘
#
★★

23. Selector 空轮询 bug 的成因(epoll bug)与 Netty 的重建 Selector 解决方案

Selector 空轮询 bug 的成因(epoll bug)与 Netty 的重建 Selector 解决方案是什么?

  • Selector 空轮询 bug
  • epoll bug
  • Netty 重建

Selector 空轮询 bug(epoll bug):Linux 上 epoll 在 select() 时可能"空轮询"——即使没有事件就绪,select() 也立即返回 0(而不是阻塞),导致 CPU 空转(busy loop),高 CPU 占用。成因:Linux epoll 的已知 bug(某些内核版本触发条件)。Netty 解决方案:检测到空轮询(select() 返回后无事件,超过阈值)则"重建 Selector"——旧 Selector 的注册的 Channel 重新注册到新 Selector,替换旧 Selector,避免空轮询继续。Netty 的 Selector 重建机制规避 epoll bug,保证事件循环不被空轮询拖死。

epoll 空轮询 bug 使 select() 无事件也返回 0 导致 CPU 空转,Netty 检测到空轮询后重建 Selector(重注册 Channel)。

// Netty 空轮询检测与重建(简化)
if (selectCnt > SELECTOR_AUTO_REBUILD_THRESHOLD) {
    rebuildSelector();   // 重建 Selector,重注册 Channel
}
#
★★

24. Charset 与 StandardCharsets 的常用编码

Charset 与 StandardCharsets 的常用编码是什么?

  • Charset
  • StandardCharsets
  • 常用编码

Charset 表示字符编码(字符集字符 ↔ 字节)。StandardCharsets(JDK 7+)提供常用编码常量:UTF_8、UTF_16、UTF_16BE、UTF_16LE、ISO_8859_1、US_ASCII。常用编码:UTF-8(现代标准,可变长,兼容 ASCII,网络/JSON 默认);UTF-16(Java 内部);ISO-8859-1(Latin-1,单字节);US-ASCII(纯 ASCII)。用 StandardCharsets.UTF_8 等避免字符串编码名("UTF-8")的拼写错误与非法编码异常。Charset.forName 可按名获取(可能 UnsupportedCharsetException)。推荐:优先 StandardCharsets 常量,默认 UTF-8。

StandardCharsets 提供 UTF_8/UTF_16/ISO_8859_1 等常量,UTF-8 是现代标准,用常量避免编码名错误。

byte[] b = "hello".getBytes(StandardCharsets.UTF_8);   // 标准常量
String s = new String(b, StandardCharsets.UTF_8);
Charset cs = Charset.forName("UTF-8");   // 按名获取
#
★★

25. ChronoUnit 与 TemporalAdjusters 在工作日计算与节假日调整中的工具方法

ChronoUnit 与 TemporalAdjusters 在工作日计算与节假日调整中的工具方法是什么?

  • ChronoUnit
  • TemporalAdjusters
  • 工作日/节假日

ChronoUnit 提供时间单位(DAYS、HOURS、WEEKS、MONTHS 等),用于计算两个时间之间的差值(between 方法)或增减(plus/minus)。TemporalAdjusters 提供时间调整器:firstDayOfMonth、lastDayOfMonth、firstInMonth、next(DayOfWeek)、previous(DayOfWeek)、with(TemporalAdjuster) 等,用于日期调整(周一、月底、下个工作日)。工作日计算:用 ChronoUnit.DAYS.between 计算天数,再排除周末(检查 DayOfWeek),TemporalAdjusters.next(DayOfWeek.MONDAY) 找下周一。节假日调整:自定义 TemporalAdjuster(判断节假日)或用工具类。方法:ChronoUnit 算差值、TemporalAdjusters 调整日期到特定天。

ChronoUnit 提供时间单位与差值计算,TemporalAdjusters 调整日期(月底/周一/下个工作日),自定义调整器处理节假日。

long days = ChronoUnit.DAYS.between(start, end);   // 差天数
LocalDate next = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.MONDAY));  // 下周一
LocalDate last = LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());        // 月底
#
★★

26. Clock 抽象(Clock.fixed/Clock.offset)的测试价值

Clock 抽象(Clock.fixed/Clock.offset)的测试价值是什么?

  • Clock 抽象
  • Clock.fixed/offset
  • 测试

Clock 抽象(java.time.Clock)提供时间源,可注入到时间 API(LocalDateTime.now(clock)、Instant.now(clock))。测试价值:Clock.fixed(instant, zone) 返回固定时间(静态时间),Clock.offset(baseClock, offset) 返回偏移时间(向前/向后偏移)。测试中用 Clock 控制时间:1)固定时间测试(Clock.fixed)使时间确定,测试可复现;2)偏移时间测试(Clock.offset)模拟过去/未来(测试超时、过期);3)依赖注入 Clock 让代码可测试(不依赖系统时间)。价值:通过注入自定义 Clock,测试代码无需等待真实时间,控制时间行为。

Clock 抽象可注入时间源,Clock.fixed 固定时间、Clock.offset 偏移时间,测试中控制时间使测试确定可复现。

Clock fixed = Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);
LocalDate d = LocalDate.now(fixed);   // 固定 2026-01-01
Clock offset = Clock.offset(Clock.systemUTC(), Duration.ofHours(1));  // 偏移 1 小时
#
★★

27. Collator 在区域敏感字符串比较的应用

Collator 在区域敏感字符串比较的应用是什么?

  • Collator
  • 区域敏感排序
  • 比较

Collator(java.text)用于区域(Locale)敏感的字符串比较,正确处理区域特定的排序规则(如中文按拼音/笔画、德语按变音、法语重音)。应用:1)Collator.getInstance(Locale) 获取区域排序器;2)compare 方法按区域规则比较字符串;3)用于排序(Collections.sort 用 Collator 比较器)、搜索。区域敏感:不同 Locale 的排序规则不同(如中文拼音),Collator 按区域规则比较,避免默认 Unicode 比较不符合区域习惯。应用场景:排序列表(按区域语言习惯)、搜索匹配。注意:Collator 非线程安全,需 ThreadLocal 或每次创建。

Collator 按 Locale 区域规则比较字符串(中文拼音/德语变音),用于区域敏感排序,非线程安全需隔离。

Collator collator = Collator.getInstance(Locale.CHINA);   // 中文排序
List<String> names = ...;
names.sort(collator);   // 按区域规则排序
int cmp = collator.compare("张", "李");
#
★★

28. Duration 与 Period 的差异(时间量 vs 日期量)

Duration 与 Period 的差异(时间量 vs 日期量)是什么?

  • Duration 时间量
  • Period 日期量
  • 差异

Duration 与 Period 都是时间量(amount of time),但差异:Duration 表示"基于时间的量"(秒、纳秒、小时、分钟,精确的时长),用于时间差(Instant 之间、LocalTime 之间);Period 表示"基于日期的量"(年、月、日,日历字段),用于日期差(LocalDate 之间)。差异:Duration 是精确时间(含纳秒),Period 是日历日期(年月日,考虑月份天数)。示例:Duration.between(t1, t2) 精确时长;Period.between(date1, date2) 年月日。选择:需要精确时长(秒/纳秒)用 Duration,需要日历日期(年/月/日)用 Period。注意:Duration 不能用于 LocalDate(日历),Period 不能用于精确时间。

Duration 是精确时间量(秒纳秒),Period 是日历日期量(年月日),按需要精确时长还是日历日期选择。

Duration d = Duration.between(instant1, instant2);   // 精确时长(秒纳秒)
Period p = Period.between(date1, date2);             // 年月日
LocalDate plus = LocalDate.now().plus(Period.ofMonths(1));
#
★★

29. ISO 8601 与 RFC 3339 的字符串互换

ISO 8601 与 RFC 3339 的字符串互换是什么?

  • ISO 8601
  • RFC 3339
  • 时间字符串

ISO 8601 是国际日期时间格式标准,RFC 3339 是 ISO 8601 的"profile"(用于 Internet 的简化子集)。两者互换:时间字符串格式 2026-08-03T10:15:30Z(UTC)或带偏移 2026-08-03T10:15:30+08:00。java.time 的对应用法:Instant.toString() 输出 ISO-8601 UTC(Z 结尾);OffsetDateTime.toString() 输出带偏移;LocalDateTime.toString() 输出无时区。解析:Instant.parse、OffsetDateTime.parse、ZonedDateTime.parse 解析 ISO/RFC 3339 格式。互换:跨系统传输用 ISO 8601/RFC 3339(UTC 带 Z),不同形式(Instant vs OffsetDateTime)可互相转换。注意 RFC 3339 要求带时区偏移(Z 或 +hh:mm),ISO 8601 更宽。

ISO 8601/RFC 3339 是时间字符串标准(UTC 带 Z 或偏移),java.time 用 Instant/OffsetDateTime 格式与解析。

Instant now = Instant.now();
String iso = now.toString();          // 2026-08-03T10:15:30Z(ISO/RFC3339 UTC)
Instant parsed = Instant.parse(iso);  // 解析
OffsetDateTime odt = now.atOffset(ZoneOffset.UTC);
String s = odt.toString();            // ...Z
#
★★

30. JDK 8 DateTimeFormatterBuilder 的复杂模式构造

JDK 8 DateTimeFormatterBuilder 的复杂模式构造是什么?

  • DateTimeFormatterBuilder
  • 复杂模式
  • 自定义格式

DateTimeFormatterBuilder 用于构建复杂的 DateTimeFormatter(不能用简单 pattern 表达或需动态拼接):appendPattern 追加简单模式、appendValue 追加字段(可指定宽度)、appendLiteral 追加字面量、appendOptional 追加可选部分、appendText 追加文本(或自定义)、parseCaseInsensitive 等。适用于:1)复杂格式(可选字段、多部分);2)解析未来/过去(appendValueReduced);3)动态构建格式;4)多格式组合。示例:DateTimeFormatterBuilder().appendPattern("yyyy-MM").appendLiteral(" ").appendValue(ChronoField.DAY_OF_MONTH).toFormatter()。它是 pattern 无法表达时的强大工具。

DateTimeFormatterBuilder 拼接复杂格式(appendPattern/appendValue/appendOptional),用于简单 pattern 无法表达的复杂/动态格式。

DateTimeFormatter fmt = new DateTimeFormatterBuilder()
    .appendPattern("yyyy-MM")
    .appendLiteral(" ")
    .appendValue(ChronoField.DAY_OF_MONTH)
    .appendOptional(DateTimeFormatter.ofPattern(" HH:mm"))
    .toFormatter();
#
★★

31. JDK 9 Locale.IsoCountryCode 与 Locale.forLanguageTag

JDK 9 Locale.IsoCountryCode 与 Locale.forLanguageTag 是什么?

  • Locale.IsoCountryCode
  • Locale.forLanguageTag
  • 语言标签

Locale.IsoCountryCode(JDK 9+)是枚举,用于从国家代码构建 Locale 时指定 ISO 国家代码标准(PART1 两字母、PART2 三字母、PART3 数字),Locale.Builder 或 Locale 构造时用。Locale.forLanguageTag(String) 按 IETF BCP 47 语言标签(如 "zh-CN"、"en-US")构建 Locale,比 new Locale(lang, country) 更规范(解析完整语言标签,含 script/region/extensions)。区别:forLanguageTag 解析 BCP 47 语言标签(标准、支持扩展),new Locale 用单独组件。应用:与 HTTP Accept-Language、CLDR 协作。

IsoCountryCode 指定国家代码标准,forLanguageTag 解析 BCP 47 语言标签构建 Locale,比 new Locale 更规范。

Locale l = Locale.forLanguageTag("zh-CN");   // 解析 BCP 47 标签
Locale l2 = new Locale.Builder().setLanguage("zh").setRegion("CN").build();
// IsoCountryCode 用于指定国家代码标准
#
★★

32. UTF-8 乱码的常见根因(BOM、多字节截断、双重编码)与沿 HTTP/数据库/文件链路定位的方法

UTF-8 乱码的常见根因(BOM、多字节截断、双重编码)与沿 HTTP/数据库/文件链路定位的方法是什么?

  • UTF-8 乱码根因
  • BOM/多字节截断/双重编码
  • 链路定位

UTF-8 乱码常见根因:1)BOM:文件开头字节序标记(EF BB BF)被当作字符显示;2)多字节截断:多字节 UTF-8 字符被按单字节截断(编码错误),产生错乱;3)双重编码:数据被编码两次(如 UTF-8 再按 ISO-8859-1 转),导致乱码;4)解码/编码不一致:读取时用错编码。沿链路定位:HTTP(Content-Type charset、请求/响应编码)、数据库(连接字符集、表/列字符集)、文件(编码声明)。定位方法:1)确认各环节编码一致(HTTP charset、DB 连接 utf8mb4、文件 UTF-8);2)用字节级检查(hexdump 看 BOM/多字节);3)区分"一次编码错误"与"双重编码"(重编码尝试);4)统一 UTF-8(含 DB 字符集、HTTP 头)。链路中任一环节编码不一致即乱码。

UTF-8 乱码根因含 BOM、多字节截断、双重编码、编码不一致,统一各环节 UTF-8 并检查 charset。

// 统一编码:读写都指定 UTF-8
String s = new String(bytes, StandardCharsets.UTF_8);
// 检查 BOM:bytes[0]==0xEF && bytes[1]==0xBB && bytes[2]==0xBF
// 数据库连接:jdbc:mysql://...?characterEncoding=utf8mb4
#
★★

33. Java 25 中字符串相关的优化与变更

Java 25 中字符串相关的优化与变更是什么?

  • Java 25 字符串优化
  • 字符串模板
  • 变更

Java 25 中字符串相关优化与变更:1)String Templates(字符串模板):此前在 JDK 21/22 以预览形式演进(JEP 430/459),JEP 465(第三次预览)已被撤回,JDK 23 起未再进入任何版本,截至 JDK 25 仍未定稿;2)String 相关性能优化(具体以版本为准);3)字符串常量池/StringTable 优化延续。注意:Java 25 是 LTS,汇总特性;字符串模板(STR 模板处理器)自 JDK 23 起被搁置,不应按已定稿特性使用,拼接与 format 仍是主流方式。

Java 25 未将字符串模板定稿(JEP 459 仅为 JDK 22 预览),拼接与 format 仍是主流,String 内部优化延续。

// String Templates(JEP 459)
String name = "World";
String msg = STR."Hello \{name}!";   // 字符串模板,内嵌表达式
#
★★

34. Java 9 Matcher.replaceAll 的流式方法

Java 9 Matcher.replaceAll 的流式方法是什么?

  • Matcher.replaceAll
  • 流式/replaceAll Function
  • Java 9

Java 9 引入 Matcher.replaceAll(Function<MatchResult, String>) 重载:用函数式替换每个匹配项(根据匹配结果生成替换串),比 replaceAll(String)(替换为固定字符串)更灵活。流式方法:replaceAll(Function) 遍历所有匹配,对每个匹配调用函数生成替换,实现"基于匹配内容的动态替换"。示例:m.replaceAll(mr -> mr.group().toUpperCase()) 把所有匹配转大写。Java 9 还引入 Matcher.replaceFirst(Function) 和 results()(返回匹配流,Stream)。流式:结果可流式处理(results() 遍历匹配)。replaceAll(Function) 动态替换,results() 流式处理匹配。

Java 9 的 replaceAll(Function) 用函数动态替换每个匹配,results() 返回匹配流,替代固定字符串替换。

Pattern p = Pattern.compile("\\b\\w+\\b");
Matcher m = p.matcher("hello world");
String s = m.replaceAll(mr -> mr.group().toUpperCase());   // "HELLO WORLD"
// results():流式处理匹配
m.results().map(MatchResult::group).toList();
#
★★

35. Java 原生序列化的版本兼容规则中,哪些字段变更向后兼容、哪些会直接抛出 InvalidClassException

Java 原生序列化的版本兼容规则中,哪些字段变更向后兼容、哪些会直接抛出 InvalidClassException?

  • 序列化版本兼容
  • 字段变更
  • InvalidClassException

Java 原生序列化版本兼容规则:serialVersionUID 相同(或未改)时,字段变更兼容性:1)向后兼容:新增字段(反序列化时用默认值)、删除字段(忽略)、字段类型不兼容会导致异常;2)不兼容/抛 InvalidClassException:serialVersionUID 不匹配(类定义变了)、字段类型不兼容(类型不匹配)、类结构无法兼容(如字段类型变化导致反序列化失败)。会抛 InvalidClassException 的情况:serialVersionUID 不一致、类不存在/不可序列化、字段类型不匹配。向后兼容:新增字段(默认值填充)、删除字段(忽略)、字段类型兼容。规则:serialVersionUID 稳定 + 字段类型不变 + 只增删字段(用默认值/忽略)兼容。

序列化兼容:serialVersionUID 一致且字段类型不变时增删字段向后兼容(默认值/忽略),serialVersionUID 不匹配或字段类型不兼容抛 InvalidClassException。

class User implements Serializable {
    private static final long serialVersionUID = 1L;   // 稳定
    String name;   // 新增字段:反序列化默认值
    int age;       // 删除字段:忽略
}
// serialVersionUID 不匹配 -> InvalidClassException
#
★★

36. Locale 的层级与默认 locale 获取

Locale 的层级与默认 locale 获取是什么?

  • Locale 层级
  • 默认 locale
  • 获取

Locale 表示语言/区域(语言、脚本、国家、变体等维度),层级:language(语言)、script(脚本)、country(国家/区域)、variant(变体)、extensions(扩展),如 Locale("zh", "CN", "Hans")。默认 locale 获取:Locale.getDefault() 返回 JVM 默认 locale(跟随系统/参数),Locale.getDefault(Locale.Category.FORMAT/DISPLAY) 按类别获取(格式/显示)。默认 locale 影响 NumberFormat、DateFormat、Collator 等。注意:默认 locale 可被程序设置(Locale.setDefault),依赖全局默认 locale 的代码可能受影响。获取:getDefault() 获取默认,构造器/Builder 指定特定 locale。

Locale 层级含语言/脚本/国家/变体/扩展,getDefault() 获取 JVM 默认 locale,按 Category 区分格式/显示。

Locale def = Locale.getDefault();                                  // 默认
Locale fmt = Locale.getDefault(Locale.Category.FORMAT);            // 格式类别
Locale cn = new Locale.Builder().setLanguage("zh").setRegion("CN").build();
#
★★

37. Locale.forLanguageTag 与 Locale.Builder 在复杂语言标签解析上的差异

Locale.forLanguageTag 与 Locale.Builder 在复杂语言标签解析上的差异是什么?

  • forLanguageTag
  • Locale.Builder
  • 复杂语言标签

Locale.forLanguageTag(String) 与 Locale.Builder 都能构建 Locale,差异:forLanguageTag 解析 IETF BCP 47 语言标签字符串(如 "zh-Hans-CN"),按标签规范解析(语言、脚本、区域、扩展),对不合规标签可能宽松处理;Locale.Builder 用链式方法逐组件设置(setLanguage、setScript、setRegion、setVariant、setExtension),提供更严格的校验(非法组件抛异常),可精确控制各组件。复杂语言标签:forLanguageTag 适合"从标准标签字符串解析"(如 HTTP Accept-Language),Builder 适合"逐组件构建并校验"。差异:forLanguageTag 解析字符串、宽松;Builder 组件式、严格校验。

forLanguageTag 解析 BCP 47 标签字符串(宽松),Locale.Builder 组件式构建(严格校验),按输入形态选择。

Locale l1 = Locale.forLanguageTag("zh-Hans-CN");   // 解析标签
Locale l2 = new Locale.Builder()
    .setLanguage("zh").setScript("Hans").setRegion("CN").build();  // 组件式
#
★★

38. MonthDay/Year/YearMonth 的工程场景

MonthDay/Year/YearMonth 的工程场景是什么?

  • MonthDay
  • Year/YearMonth
  • 工程场景

MonthDay(月日,无年):表示"每年相同月日"的确定日期(如生日、节日、周年),用于"无序年的月日"比较/判断;Year(年):表示单纯的年份,用于年龄计算、年份区间;YearMonth(年月):表示"年-月",用于按月统计、月度账单、信用卡有效期、排班。工程场景:MonthDay 处理生日/节日(忽略年);Year 处理年份维度;YearMonth 处理月度数据(月度报表、账单周期)。示例:MonthDay.of(12, 25) 表示圣诞节;YearMonth.now() 当月;Year 判断闰年。用于特定"部分日期"的建模。

MonthDay 表示月日(生日/节日)、Year 表示年份、YearMonth 表示年月(月度统计),用于部分日期建模。

MonthDay birthday = MonthDay.of(12, 25);   // 生日(忽略年)
YearMonth ym = YearMonth.now();            // 当月
Year year = Year.of(2026);                 // 年份
year.isLeap();                             // 闰年判断
#
★★

39. ObjectInputFilter 的 maxdepth/maxrefs/maxbytes 限制与黑白名单模式如何在 JEP 290/415 框架下构成纵深防御

ObjectInputFilter 的 maxdepth/maxrefs/maxbytes 限制与黑白名单模式如何在 JEP 290/415 框架下构成纵深防御?

  • ObjectInputFilter
  • JEP 290/415
  • 纵深防御

ObjectInputFilter(JEP 290/415)是反序列化安全过滤器,限制反序列化。主要限制:maxdepth(最大对象深度)、maxrefs(最大引用数)、maxbytes(最大字节数),以及模式(黑白名单:允许/拒绝类)。JEP 290(反序列化过滤器,JDK 9)与 JEP 415(JDK 17 增强)框架下,ObjectInputFilter 构成纵深防御:1)资源限制:maxdepth/maxrefs/maxbytes 防止反序列化构造深/大/多对象耗尽资源(缓解 DoS);2)类限制:黑白名单阻止危险类(gadget 类)反序列化;3)组合:资源限制 + 类限制 + 全局/自定义过滤器层层防御。纵深防御:即便有反序列化入口,过滤器限制资源与类,降低攻击面。通过 ObjectInputFilter.Config 配置或自定义。

ObjectInputFilter 的 maxdepth/maxrefs/maxbytes 限制资源、黑白名单限制类,JEP 290/415 框架下构成反序列化纵深防御。

// 配置反序列化过滤器(JEP 290/415)
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "maxdepth=10;maxrefs=100;maxbytes=100000;java.util.*;!com.example.*");
ObjectInputStream ois = new ObjectInputStream(in);
ois.setObjectInputFilter(filter);   // 应用过滤器
#
★★

40. Pattern.UNICODE_CHARACTER_CLASS 与 Unicode 属性

Pattern.UNICODE_CHARACTER_CLASS 与 Unicode 属性是什么?

  • UNICODE_CHARACTER_CLASS
  • Unicode 属性
  • 正则

Pattern.UNICODE_CHARACTER_CLASS(UNICODE_CHARACTER_CLASS flag)使预定义字符类(\d、\w、\s、\b 等)按 Unicode 语义匹配,而非仅 ASCII。默认 \d 只匹配 0-9(ASCII),启用 UNICODE_CHARACTER_CLASS 后 \d 匹配 Unicode 数字(如全角数字)。Unicode 属性:正则可用 \p{...} 匹配 Unicode 属性(如 \p{L} 字母、\p{Lu} 大写字母、\p{IsEmoji} 等),按 Unicode 类别/脚本匹配。Unicode 属性更精确地按 Unicode 数据匹配。应用:处理多语言文本(中文、其他文字)时用 UNICODE_CHARACTER_CLASS 或 \p{...} 匹配 Unicode 字符。注意启用后性能与语义变化。

UNICODE_CHARACTER_CLASS 让预定义类按 Unicode 语义匹配(非仅 ASCII),\p{...} 匹配 Unicode 属性,用于多语言文本。

Pattern p = Pattern.compile("\\w+", Pattern.UNICODE_CHARACTER_CLASS);  // Unicode 单词字符
Pattern p2 = Pattern.compile("\\p{L}+");   // Unicode 字母
#
★★

41. Pattern.asPredicate 与流式协作

Pattern.asPredicate 与流式协作是什么?

  • Pattern.asPredicate
  • 流式
  • Predicate

Pattern.asPredicate() 返回一个 Predicate,把正则表达式转换为谓词(test 方法是匹配输入),用于流式过滤:stream.filter(Pattern.compile("...").asPredicate())。流式协作:把正则匹配作为 Predicate 用于 Stream.filter,简洁地过滤匹配项。示例:list.stream().filter(Pattern.compile("\d+").asPredicate()).toList() 只保留数字字符串。asPredicate 让正则复用 Predicate 接口(流式、组合)。注意:asPredicate 的 test 等价于 matcher(input).find()(部分匹配,非全匹配)。用于流式过滤。

Pattern.asPredicate() 把正则转 Predicate,test 用 find() 匹配,用于 Stream.filter 流式过滤。

Predicate<String> isDigit = Pattern.compile("\\d+").asPredicate();
List<String> nums = list.stream().filter(isDigit).toList();   // 留数字
#
★★

42. Scanner 的局限与生产禁用

Scanner 的局限与生产禁用是什么?

  • Scanner 局限
  • 生产禁用
  • 替代

Scanner 用于简单文本解析(读取 token、分隔),但局限:1)性能差:Scanner 用正则做分隔,解析慢,不适合大规模输入;2)依赖正则:next()/nextInt() 逐 token 正则匹配,性能与健壮性不佳;3)不捕获原生异常(IOException 被吞);4)生产禁用:大数据/高并发/生产环境的输入解析不用 Scanner(性能、健壮性),用 BufferedReader/Stream(高性能)或专门解析器(JSON 解析器等)。Scanner 适合教学、简单交互命令行,生产用 BufferedReader(性能)或专用解析器。

Scanner 用正则解析慢、性能差、吞异常,生产应避免,用 BufferedReader/Stream 或专用解析器。

// 生产禁用:Scanner 慢
Scanner sc = new Scanner(System.in);
// 替代:BufferedReader 高性能
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
    String line = br.readLine();
}
#
★★

43. String.format 与 MessageFormat 的本地化取舍

String.format 与 MessageFormat 的本地化取舍是什么?

  • String.format
  • MessageFormat
  • 本地化

String.format 与 MessageFormat 都格式化字符串,取舍:String.format 用 %s/%d 等格式说明符,灵活但本地化差(参数顺序固定,难本地化);MessageFormat 支持本地化(Locale,参数按 {0}/{1} 占位,可调整顺序、复数/选择),用于国际化消息(配合 ResourceBundle)。取舍:需要本地化(i18n、多语言、复数处理)用 MessageFormat(占位符 + Locale),简单格式用 String.format。MessageFormat 适合"消息模板 + 本地化",String.format 适合"技术性格式"(数字、日期)。选择:业务消息交互用 MessageFormat/ResourceBundle,简单拼接用 String.format 或模板。

String.format 用 %s 格式(本地化差),MessageFormat 用 {0} 占位符支持 Locale/复数/顺序调整,适合国际化消息。

// String.format:%s 固定顺序
String s1 = String.format("Hello %s, age %d", name, age);
// MessageFormat:{0} 占位,可调顺序/本地化
String s2 = MessageFormat.format("Hello {0}, age {1}", name, age);
#
★★

44. String.isBlank 与 String.strip 的 Unicode 空白处理

String.isBlank 与 String.strip 的 Unicode 空白处理是什么?

  • isBlank
  • strip
  • Unicode 空白

String.isBlank()(JDK 11+)判断字符串是否只含空白(空或全空白),返回 boolean;String.strip() 去除首尾空白。两者都基于 Character.isWhitespace(Unicode 空白),但注意:1)isBlank 用 Character.isWhitespace(Unicode 空白,含全角空格、制表符等),trim 只处理 <= 0x20 的 ASCII 空白;2)strip 用 Character.isWhitespace(Unicode 空白),trim 只处理 ASCII 空白。差异:isBlank/strip 是 Unicode 空白(Character.isWhitespace),trim 是 ASCII 空白。Unicode 空白处理:isBlank/strip 正确处理 Unicode 空白(如全角空格、nbsp 的部分),trim 只处理 ASCII。选择:需要 Unicode 空白用 isBlank/strip。

isBlank/strip 基于 Character.isWhitespace(Unicode 空白),trim 只处理 ASCII 空白,Unicode 空白场景用 isBlank/strip。

String s = "  \u3000 abc \u3000 ";   // 含全角空格
s.isBlank();   // false(有内容)
s.strip();     // "abc"(Unicode 空白去除)
s.trim();      // "  \u3000 abc \u3000 "(trim 不处理全角空格)
#
★★

45. String.join 与 Collectors.joining 的取舍

String.join 与 Collectors.joining 的取舍是什么?

  • String.join
  • Collectors.joining
  • 取舍

String.join(delimiter, iterable/array) 与 Collectors.joining(delimiter) 都用于拼接字符串(用分隔符连接)。取舍:String.join 适合"直接拼接集合/数组"(简单,非流式);Collectors.joining 适合"Stream 管道中拼接"(配合 map/filter 等,可加 prefix/suffix)。joining 支持 prefix/suffix(三参重载),String.join 只支持分隔符。选择:已有集合/数组直接 String.join;Stream 中处理后再拼接用 Collectors.joining。两者底层类似(StringJoiner)。示例:String.join(",", list);list.stream().map(...).collect(Collectors.joining(",", "[", "]"))。

String.join 直接拼接集合(简单),Collectors.joining 用于 Stream 管道(支持 prefix/suffix),按上下文选择。

String s1 = String.join(",", list);   // 直接拼接
String s2 = list.stream().map(x -> x.toUpperCase()).collect(Collectors.joining(",", "[", "]"));  // 流式+前后缀
#
★★

46. String.lines 与 Files.readAllLines 的取舍

String.lines 与 Files.readAllLines 的取舍是什么?

  • String.lines
  • Files.readAllLines
  • 取舍

String.lines()(JDK 11+)把字符串按行拆分返回 Stream (惰性分割,不创建数组);Files.readAllLines(path) 读取文件所有行返回 List(一次性读入内存)。取舍:String.lines 处理"已加载的字符串"按并行流式处理(不必先拆数组);Files.readAllLines 读取"文件"所有行(适合小文件,一次性)。大文件:Files.readAllLines 一次性读入内存,大文件消耗内存,应用 Files.lines()(惰性流式读)。选择:处理字符串用 String.lines,读小文件用 readAllLines,读大文件用 Files.lines()(流式)。String.lines 惰性、Files.readAllLines 急切。

String.lines 惰性拆分字符串行,Files.readAllLines 一次性读文件行(小文件),大文件用 Files.lines() 流式。

String.lines().forEach(...);        // 惰性拆分字符串
Files.readAllLines(path).forEach(...);   // 一次性读文件(小文件)
try (Stream<String> s = Files.lines(path)) { ... }   // 大文件流式
#
★★

47. String.repeat(int) 的实现复杂度

String.repeat(int) 的实现复杂度是什么?

  • String.repeat
  • 实现复杂度
  • 性能

String.repeat(count)(JDK 11+)重复字符串 count 次,实现复杂度:JDK 11 的 repeat 用"倍增"(exponentiation by squaring)优化:重复次数按二进制展开,每次复制翻倍(如 repeat(5) = 复制 1 次成 2,再成 4,再补 1),时间复杂度 O(count)(复制总量),但通过倍增避免逐次 +1 的重复复制(O(count) 总复制量,但步骤 O(log count) 次复制)。实际复杂度:repeat 生成的字符串总长度 O(n),复制总量 O(n),是线性最优(相对逐次拼接的 O(n²))。repeat 高效:倍增复制减少重复复制,复杂度 O(n)(n 结果长度)。

String.repeat 用倍增复制(指数平方),总复制量 O(n)(结果长度),避免逐次拼接的 O(n²),高效。

String s = "ab".repeat(3);   // "ababab"
// 实现:倍增复制(5 -> 2+2+1),总复制 O(n)
#
★★

48. String.split 的限制(尾部空字符串)与 Splitter(Guava)

String.split 的限制(尾部空字符串)与 Splitter(Guava)是什么?

  • String.split 限制
  • 尾部空字符串
  • Guava Splitter

String.split 的限制:1)默认丢弃尾部空字符串("a,b,".split(",") 返回 ["a","b"],丢弃尾部空);2)用正则分隔(参数是正则,需转义特殊字符);3)-1 限制可保留尾部空(split(regex, -1))。Guava Splitter 克服限制:Splitter.on(",").splitToList(s) 保留尾部空字符串、可配置(trimResults、omitEmptyStrings、limit)、性能好、可读。取舍:简单分隔用 String.split(注意尾部空与正则转义),需保留尾部空/多配置/性能用 Guava Splitter。Splitter 是 Guava 的健壮拆分工具。

String.split 默认丢弃尾部空字符串、参数是正则(需转义),Splitter 保留尾部空、可配置(trim/omitEmpty/limit)更健壮。

"a,b,".split(",");            // ["a","b"](丢弃尾部空)
"a,b,".split(",", -1);        // ["a","b",""](保留尾部空)
List<String> l = Splitter.on(",").trimResults().omitEmptyStrings().splitToList("a, ,b");
#
★★

49. StringJoiner 与 String.join 的底层实现差异是什么,Stream 的 Collectors.joining 又复用了哪个组件

StringJoiner 与 String.join 的底层实现差异是什么?Stream 的 Collectors.joining 复用了哪个组件?

  • StringJoiner
  • String.join
  • Collectors.joining 复用

StringJoiner 与 String.join 底层:String.join 内部用 StringJoiner 实现(传入分隔符),StringJoiner 是 StringBuilder 包装(add 用 StringBuilder.append,支持 prefix/suffix/delem)。差异:String.join 是便捷静态方法(用 StringJoiner),StringJoiner 是更灵活的类(可 add 任意、可配置 prefix/suffix、可合并 merge)。Collectors.joining 复用 StringJoiner:Collectors.joining 的 collector 用 StringJoiner 作为累加容器(supplier 返回 StringJoiner,accumulator 用 add,combiner 用 merge),最终 toString。三者都基于 StringBuilder/StringJoiner(可变拼接),joining 复用 StringJoiner。

String.join 内部用 StringJoiner,Collectors.joining 复用 StringJoiner 作累加容器,三者都基于可变拼接。

// String.join 内部用 StringJoiner
StringJoiner sj = new StringJoiner(",", "[", "]");
sj.add("a").add("b").toString();   // "[a,b]"
// Collectors.joining 复用 StringJoiner
String s = list.stream().collect(Collectors.joining(",", "[", "]"));
#
★★

50. StringTokenizer 的遗留问题

StringTokenizer 的遗留问题是什么?

  • StringTokenizer
  • 遗留问题
  • 替代

StringTokenizer 是遗留的字符串分割类(JDK 1.0),遗留问题:1)API 不友好(hasMoreTokens/nextToken 枚举式,非标准集合/流式);2)不支持正则(只能用分隔符字符集,不能按正则分割);3)行为与 split 不一致(split 用正则,tokenizer 用分隔符字符集);4)开发实践中被 String.split 或 Guava Splitter 替代。替代:String.split(正则)或 Guava Splitter。StringTokenizer 是遗留,新代码应避免,用 split/Scanner/Splitter。遗留问题:API 古老、无正则、与新代码不兼容。

StringTokenizer 是遗留分割类,API 不友好、无正则、行为不一致,新代码用 String.split 或 Guava Splitter 替代。

// 遗留:StringTokenizer
StringTokenizer st = new StringTokenizer("a,b,c", ",");
while (st.hasMoreTokens()) { st.nextToken(); }
// 替代:String.split 或 Splitter
String[] parts = "a,b,c".split(",");
#
★★

51. TemporalAdjusters 的常见用法(月初、月底、周一)

TemporalAdjusters 的常见用法(月初、月底、周一)是什么?

  • TemporalAdjusters
  • 月初/月底/周一
  • 用法

TemporalAdjusters 提供日期调整方法:firstDayOfMonth()(月初)、lastDayOfMonth()(月底:lastDayOfMonth)、firstDayOfNextMonth()(下月月初)、next(DayOfWeek.MONDAY)(下周一)、previous(DayOfWeek.MONDAY)(上周一)、firstInMonth(DayOfWeek)(本月第一个周一)、dayOfWeekInMonth、nextOrSame 等。用法:date.with(TemporalAdjusters.firstDayOfMonth()) 得到月初;with(lastDayOfMonth()) 月底;with(next(DayOfWeek.MONDAY)) 下周一。适用于报表周期、任务调度(月初/月底/每周一)。TemporalAdjusters 让日期调整声明式。

TemporalAdjusters 提供 firstDayOfMonth/lastDayOfMonth/next(MONDAY) 等,date.with(...) 调整到月初/月底/周一。

LocalDate d = LocalDate.now();
LocalDate first = d.with(TemporalAdjusters.firstDayOfMonth());   // 月初
LocalDate last = d.with(TemporalAdjusters.lastDayOfMonth());     // 月底
LocalDate mon = d.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); // 下周一
#
★★

52. TimeUnit 与 Duration 的转换

TimeUnit 与 Duration 的转换是什么?

  • TimeUnit
  • Duration
  • 转换

TimeUnit 与 Duration 都是时间单位/量,互相转换:TimeUnit 是枚举(NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS、MINUTES、HOURS、DAYS),提供 convert、toSeconds 等转换,适合"把数值在不同时间单位间转换"(如 TimeUnit.SECONDS.toMillis(5));Duration 是时间量对象(秒+纳秒),提供 toNanos/toMillis/toSeconds 与 between(两时间差)。转换:TimeUnit 用于"单位换算"(数值),Duration 用于"时间量"(两个时间差或时长)。TimeUnit 辅助 Duration:Duration.ofMillis(100) 创建,d.toMillis() 获取。TimeUnit 侧重单位换算,Duration 侧重时间量。

TimeUnit 是时间单位枚举(单位换算),Duration 是时间量对象(时长/时间差),互相用 toXxx/ofXxx 转换。

long ms = TimeUnit.SECONDS.toMillis(5);       // 5000(单位换算)
Duration d = Duration.ofMillis(5000);          // 时间量
d.toSeconds();                                  // 5
Duration between = Duration.between(t1, t2);
#
★★

53. Unicode 规范化(NFC/NFD)对字符串比较的影响

Unicode 规范化(NFC/NFD)对字符串比较的影响是什么?

  • Unicode 规范化
  • NFC/NFD
  • 字符串比较

Unicode 规范化(NFC/NFD)把等价的 Unicode 字符序列统一为规范形式:NFC(Normalization Form C,组合形式,如 é 用单个组合字符)、NFD(Normalization Form D,分解形式,如 é 分解为 e + 组合重音)。对字符串比较影响:两个"视觉等价"的字符串可能因组合/分解不同而字节不同(如 "é" 用组合字符 vs "e" + 组合重音),直接比较不相等。规范化后比较(统一 NFC 或 NFD)才能正确比较等价字符串。应用:文本处理、搜索、验证、规范化存储。Java 用 Normalizer.normalize(str, Normalizer.Form.NFC)。影响:跨输入(组合/分解混合)比较需规范化。

Unicode 组合/分解形式(NFC/NFD)使等价字符串字节不同,Normalizer 规范化后比较避免误判不等。

String a = "\u00e9";                 // é(组合字符)
String b = "e\u0301";                // e + 组合重音(分解)
a.equals(b);                          // false(字节不同)
Normalizer.normalize(a, Normalizer.Form.NFC).equals(
    Normalizer.normalize(b, Normalizer.Form.NFC));   // true(规范化后等价)
#
★★

54. java.time 与数据库时间类型的映射(JDBC 4.2)

java.time 与数据库时间类型的映射(JDBC 4.2)是什么?

  • java.time 映射
  • JDBC 4.2
  • 数据库时间类型

JDBC 4.2(Java 8)支持 java.time 类型与数据库时间类型的映射:LocalDate ↔ DATE、LocalTime ↔ TIME、LocalDateTime ↔ TIMESTAMP、OffsetDateTime ↔ TIMESTAMP WITH TIME ZONE、Instant ↔ TIMESTAMP(经转换)。映射规则:1)LocalDate 存 DATE(日期);2)LocalTime 存 TIME(时间);3)LocalDateTime 存 TIMESTAMP(日期时间);4)OffsetDateTime 存 TIMESTAMP WITH TIME ZONE;5)Instant 需转换(通常存 UTC 或经 OffsetDateTime)。使用:PreparedStatement.setObject(1, localDate) 与 ResultSet.getObject(col, LocalDate.class)(JDBC 4.2 直接支持)。最佳实践:存储用 UTC(Instant/OffsetDateTime)跨时区,配合数据库 TIMESTAMP WITH TIME ZONE。

JDBC 4.2 映射 java.time 到数据库类型(LocalDate-DATE、LocalDateTime-TIMESTAMP 等),setObject/getObject 直接使用。

ps.setObject(1, LocalDate.now());               // DATE
rs.getObject("d", LocalDate.class);
ps.setObject(2, OffsetDateTime.now());          // TIMESTAMP WITH TIME ZONE
#
★★

55. java.time(JSR-310)的核心类(LocalDateTime/ZonedDateTime/Instant)

java.time(JSR-310)的核心类(LocalDateTime/ZonedDateTime/Instant)是什么?

  • LocalDateTime
  • ZonedDateTime
  • Instant

java.time(JSR-310)核心类:LocalDateTime(本地日期时间,无时区,如 "2026-08-03T10:15"),用于"本地语义"(不考虑时区);ZonedDateTime(带时区的日期时间,含 ZoneId,如 "2026-08-03T10:15+08:00[Asia/Shanghai]"),用于"有时区语义"(跨时区、展示);Instant(UTC 时间戳,秒+纳秒,独立于时区),用于"时间点"(持久化、时间戳)。三者用于不同场景:LocalDateTime 本地时间(无时区操作)、ZonedDateTime 时区时间(跨时区、夏令时)、Instant 时间点(UTC 存储、Instant 比较)。转换:LocalDateTime.atZone(zone) 转 ZonedDateTime,ZonedDateTime.toInstant() 转 Instant,Instant 是时间线的点。

LocalDateTime 无时区本地时间、ZonedDateTime 带时区时间、Instant UTC 时间点,按是否有时区语义/时间点选择。

LocalDateTime ldt = LocalDateTime.now();                    // 无时区
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));  // 带时区
Instant instant = Instant.now();                             // UTC 时间点
ZonedDateTime z = ldt.atZone(ZoneId.of("Asia/Shanghai"));   // 转换
Instant i = z.toInstant();
#
★★

56. record 的序列化为何按组件名直接赋值而不走规范构造器,紧凑构造器里的参数校验在反序列化时是否仍会执行

record 的序列化为何按组件名直接赋值而不走规范构造器?紧凑构造器里的参数校验在反序列化时是否仍会执行?

  • record 序列化
  • 组件名赋值
  • 构造器校验

record 的序列化(Java 14+)反序列化时按组件名直接赋值(record 特殊的序列化机制),不走规范构造器/紧凑构造器。原因:record 序列化是"组件级"的——序列化时记录组件名与值,反序列化时按组件名直接赋值(通过特殊的 record 反序列化路径),绕过构造器,以保证反序列化与构造器解耦(且避免构造器副作用/校验意外)。因此:紧凑构造器里的参数校验在反序列化时不会执行(反序列化不走构造器)。这意味着反序列化可能创建不满足构造器不变式的 record(若序列化数据被篡改)。因此 record 反序列化的校验需另行处理(如 readObject 或序列化回环检查)。

record 反序列化按组件名直接赋值,不走构造器,紧凑构造器校验不执行,反序列化校验需另行处理。

record Point(int x, int y) {
    Point { if (x < 0) throw new IllegalArgumentException(); }  // 构造器校验
}
// 反序列化:按组件名直接赋值,不走构造器,校验不执行
#
★★

57. 为何 RPC 与缓存场景普遍弃用 Java 原生序列化而选 Protobuf/Kryo,从体积、速度、跨语言、安全四个维度如何比较

为何 RPC 与缓存场景普遍弃用 Java 原生序列化而选 Protobuf/Kryo?从体积、速度、跨语言、安全四个维度如何比较?

  • Java 原生序列化
  • Protobuf/Kryo
  • 四维度比较

RPC 与缓存普遍弃用 Java 原生序列化,选 Protobuf/Kryo,四维度比较:1)体积:Java 原生序列化含描述符(类名、字段名、句柄),体积大;Protobuf 用紧凑二进制(字段编号+类型),体积小;Kryo 紧凑。2)速度:Java 原生序列化反射驱动、慢;Protobuf 生成代码直接读写,快;Kryo 用注册/缓存,快。3)跨语言:Java 原生只 Java;Protobuf 跨语言(生成多语言代码);Kryo 主要 Java。4)安全:Java 原生序列化有反序列化 gadget 攻击风险(不安全);Protobuf 类型安全、无任意反序列化风险;Kryo 需注意。结论:Protobuf/Kryo 体积小、速度快、跨语言/安全,故 RPC/缓存用它们。

Java 原生序列化体积大、慢、仅 Java、不安全;Protobuf/Kryo 体积小、快、跨语言/安全,故 RPC/缓存弃用原生。

// Protobuf:紧凑二进制、跨语言、生成代码
Message msg = Message.parseFrom(bytes);
// Kryo:注册/缓存,紧凑
kryo.writeObject(output, obj);
#
★★

58. 反序列化 gadget 链由哪些环节构成(可序列化入口类、回调方法、危险 sink),如何从依赖裁剪角度削减可利用类

反序列化 gadget 链由哪些环节构成(可序列化入口类、回调方法、危险 sink)?如何从依赖裁剪角度削减可利用类?

  • gadget 链
  • 入口类/回调/sink
  • 依赖裁剪

反序列化 gadget 链(攻击链)由环节构成:1)可序列化入口类:反序列化时实例化的类(readObject/readResolve 等触发);2)回调方法:入口类反序列化时调用的方法(如 readObject 中调用某些方法,触发后续);3)危险 sink:最终执行危险操作的方法(如命令执行 Runtime.exec、JNDI 查找、反射调用)。攻击者构造恶意序列化数据,让入口类反序列化触发回调 → 链式调用 → 到达危险 sink 执行任意代码。依赖裁剪削减可利用类:1)移除危险/不必要的依赖(减少 gadget 类);2)禁用不必要序列化(不实现 Serializable);3)避免使用已知危险类(如 Java 反序列化库);4)用白名单过滤(反序列化过滤器)。裁剪依赖减少可作为 gadget 的类,降低攻击面。

gadget 链由入口类→回调→危险 sink 构成,依赖裁剪移除危险类、白名单过滤削减可利用类,降低攻击面。

// 依赖裁剪:移除不必要的危险依赖
// 白名单过滤:ObjectInputFilter 只允许安全类
ObjectInputFilter f = ObjectInputFilter.Config.createFilter("java.util.*;!com.danger.*");
#
★★

59. 同步/异步、阻塞/非阻塞四象限的语义区分与常见误解

同步/异步、阻塞/非阻塞四象限的语义区分与常见误解是什么?

  • 同步/异步
  • 阻塞/非阻塞
  • 四象限

同步/异步与阻塞/非阻塞是两个独立维度,构成四象限:1)同步阻塞(BIO):调用者等待结果,I/O 阻塞线程;2)同步非阻塞(NIO 轮询):调用者主动检查结果,I/O 不阻塞(但调用者轮询);3)异步阻塞(AIO + Future.get):调用异步操作,但 get() 阻塞等待结果;4)异步非阻塞(AIO 回调/CompletableFuture):调用异步操作,完成回调,不阻塞。常见误解:把同步/异步(是否等待结果)与阻塞/非阻塞(是否阻塞线程)混为一谈。同步指调用者等待结果,异步指通过回调/通知获取结果;阻塞指线程被占用等待,非阻塞指线程不等待。正确区分四象限,避免"异步一定非阻塞"的误解。

同步/异步是"结果获取方式",阻塞/非阻塞是"线程是否等待",四象限组合(BIO 同步阻塞、NIO 同步非阻塞、AIO 异步),避免混淆。

// 同步阻塞:BIO(阻塞等待结果)
// 同步非阻塞:NIO 轮询(不阻塞但主动检查)
// 异步阻塞:Future.get()(异步发起,阻塞等待)
// 异步非阻塞:CompletionHandler 回调(异步,不阻塞)
#
★★

60. 夏令时跳变对时间计算的影响(LocalDateTime 加 24 小时?)

夏令时跳变对时间计算的影响(LocalDateTime 加 24 小时?)是什么?

  • 夏令时(DST)
  • LocalDateTime 加 24 小时
  • ZonedDateTime

夏令时(DST)跳变时,某些时区时间会跳跃(如春季跳时 2:00 变 3:00,秋季回拨 1 小时)。影响:LocalDateTime(无时区)加 24 小时是"加 24 个本地小时",不感知夏令时;若实际要"加一天"(同一时刻的次日),在夏令时跳变时 LocalDateTime.plusDays(1)(按日历日)与 plusHours(24)(按 24 小时)结果不同。正确做法:跨时区/夏令时用 ZonedDateTime(感知时区与 DST),ZonedDateTime.plusDays(1) 会正确处理夏令时(保持本地时刻,跳变时调整)。LocalDateTime 加 24 小时不感知 DST,可能导致错误时刻。

LocalDateTime 加 24 小时不感知夏令时,跨时区/夏令时用 ZonedDateTime.plusDays 正确处理 DST 跳变。

ZonedDateTime zdt = ZonedDateTime.of(2026, 3, 8, 1, 30, 0, 0, ZoneId.of("America/New_York"));
zdt.plusDays(1);   // 正确处理夏令时,保持本地时刻
// LocalDateTime 加 24 小时不感知 DST
#
★★

61. 大量调用 String.intern 为何可能拖慢查找甚至引发 OOM,StringTable 大小(-XX:StringTableSize)如何调优

大量调用 String.intern 为何可能拖慢查找甚至引发 OOM?StringTable 大小(-XX:StringTableSize)如何调优?

  • String.intern
  • StringTable 查找
  • -XX:StringTableSize

大量调用 String.intern:intern 把所有字符串放入 StringTable(哈希表),若唯一字符串数量巨大:1)StringTable 桶冲突多(哈希冲突),查找变慢(桶内线性扫描);2)StringTable 固定大小(默认约 60013 桶),大量唯一字符串导致桶冲突,查找 O(n) 退化;3)intern 字符串常驻(StringTable 根部引用),不回收,内存持续增长,可能 OOM。调优:-XX:StringTableSize 调整 StringTable 桶数(增大桶数减少冲突),如 -XX:StringTableSize=1000003(素数)。适合"已知大量唯一字符串需 intern"的场景。避免:不要对大量动态唯一字符串 intern(内存/查找问题),用普通 Map 或限制。

大量 intern 使 StringTable 桶冲突、查找变慢、字符串常驻内存可能 OOM,用 -XX:StringTableSize 增大桶数减少冲突。

// 调优:增大 StringTable 桶数
// java -XX:StringTableSize=1000003 -jar app.jar
// 避免大量唯一字符串 intern
String s = "abc".intern();   // 谨慎,常驻内存
#
★★

62. serialVersionUID 的作用、默认生成算法与显式声明的工程规范

serialVersionUID 的作用、默认生成算法与显式声明的工程规范是什么?

  • serialVersionUID 作用
  • 默认生成算法
  • 显式声明规范

serialVersionUID 是序列化版本号,用于验证反序列化时类的版本是否一致:若类定义变化导致 serialVersionUID 变化,反序列化旧数据抛 InvalidClassException。默认生成算法:若未显式声明,JVM 根据类结构(类名、字段、方法、修饰符等)自动计算 serialVersionUID(基于类信息的哈希),类结构变化(增删字段/方法)会改变默认 serialVersionUID,导致反序列化旧数据失败。工程规范:显式声明 serialVersionUID(private static final long serialVersionUID = 1L 或固定值),避免依赖易变的默认算法,使版本控制可控;字段变更时显式管理版本(增删字段保持兼容)。显式声明是序列化类的工程规范。

serialVersionUID 验证版本,默认按类结构自动计算(易变),工程规范是显式声明固定值,避免类结构变化破坏反序列化。

class User implements Serializable {
    private static final long serialVersionUID = 1L;   // 显式声明,版本可控
    String name;
}
#
★★

63. SimpleDateFormat 线程不安全的原因与替代方案(ThreadLocal 缓存/DateTimeFormatter)

SimpleDateFormat 线程不安全的原因与替代方案(ThreadLocal 缓存/DateTimeFormatter)是什么?

  • SimpleDateFormat 线程不安全原因
  • ThreadLocal 缓存
  • DateTimeFormatter

SimpleDateFormat 线程不安全原因:内部有可变状态(Calendar 字段、格式状态),format/parse 修改这些状态,多线程共享时状态互踩,导致错误结果或异常。替代方案:1)ThreadLocal 缓存:每个线程一个 SimpleDateFormat 实例(ThreadLocal.withInitial 创建),避免共享,线程隔离;2)DateTimeFormatter:不可变、线程安全(JDK 8+),可静态共享,是推荐替代。选择:用 DateTimeFormatter(不可变线程安全)替代 SimpleDateFormat;若需用 SimpleDateFormat,用 ThreadLocal 隔离。DateTimeFormatter 是最终推荐,线程安全且功能全。

SimpleDateFormat 内部可变状态致线程不安全,用 ThreadLocal 隔离或 DateTimeFormatter(不可变线程安全)替代。

// ThreadLocal 隔离
ThreadLocal<SimpleDateFormat> tl = ThreadLocal.withInitial(
    () -> new SimpleDateFormat("yyyy-MM-dd"));
// 推荐:DateTimeFormatter 不可变线程安全
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
#
★★

64. 序列化代理模式(Serialization Proxy)如何借助 writeReplace/readResolve 规避默认序列化在对象不变量与安全性上的风险

序列化代理模式(Serialization Proxy)如何借助 writeReplace/readResolve 规避默认序列化在对象不变量与安全性上的风险?

  • 序列化代理模式
  • writeReplace/readResolve
  • 不变量/安全

序列化代理模式:用 writeReplace 把对象序列化为"代理对象"(不包含原始对象,只含必要字段),readResolve 在反序列化时把代理转换回原始对象。优点:1)规避默认序列化风险:代理只序列化必要字段,不暴露/不序列化内部实现细节(final 字段、深拷贝、防御性拷贝);2)不变量:反序列化时通过代理的构造器/readResolve 重建对象,可重新校验不变量(默认序列化直接赋值绕过构造器);3)安全性:代理避免序列化缓存/内部状态,防篡改。writeReplace(序列化时代理)、readResolve(反序列化恢复)。这是序列化的防御性最佳实践。

序列化代理用 writeReplace 序列化代理、readResolve 恢复,通过代理重建对象可校验不变量、不暴露内部、更安全。

class Person implements Serializable {
    private final String name;
    private Object writeReplace() { return new Proxy(this); }   // 序列化为代理
    private static class Proxy implements Serializable {
        String name;
        Proxy(Person p) { this.name = p.name; }
        private Object readResolve() { return new Person(name); }  // 恢复,可校验
    }
}
#

65. String.formatted(JDK 15+)与文本块组合在模板渲染中的应用

String.formatted(JDK 15+)与文本块组合在模板渲染中的应用是什么?

  • String.formatted
  • 文本块
  • 模板渲染

String.formatted(JDK 15+)是 String 的实例方法,用格式说明符格式化字符串(等价 String.format(s, args)),可配合文本块(Text Block)做模板渲染:多行文本块中用 %s/%d 占位,调用 formatted 填充参数。应用:SQL 模板、HTML 模板、邮件模板、报告模板的多行文本 + 格式化渲染。示例:"""SELECT * FROM %s WHERE id = %d""".formatted(table, id)。相比手动拼接,formatted + 文本块更清晰。注意:formatted 是格式化(%s),不是模板内嵌表达式(JDK 25 的字符串模板是另一机制)。用于模板渲染需注意转义(% 转义 %%)。

String.formatted 用 %s 占位格式化字符串,配合文本块做多行模板渲染(SQL/HTML/邮件模板),比拼接清晰。

String sql = """
    SELECT * FROM %s
    WHERE age > %d
    """.formatted(table, 18);
#

66. 字符串常量池(String Constant Pool)的运行机制与 intern

字符串常量池(String Constant Pool)的运行机制与 intern 是什么?

  • 字符串常量池
  • intern
  • 运行机制

字符串常量池(String Constant Pool)是 JVM 维护的字符串池:编译期字面量字符串放入常量池(JDK 7+ 在堆中 StringTable),相同的字面量复用同一对象("abc" == "abc" 为 true)。运行机制:编译期把字面量放入常量池,运行时 StringTable 维护;intern() 把字符串加入常量池并返回池中引用(若有则返回池中对象)。intern 用途:显式复用字符串(减少重复对象),但大量 intern 有内存/查找问题。运行机制:字面量编译期入池,new String 创建新对象(不入池),intern 显式入池。常量池使相同字面量引用复用。

字符串常量池编译期放入字面量(StringTable 在堆),相同字面量复用,intern 显式入池返回池中引用。

String a = "abc";   // 字面量,入常量池
String b = "abc";
a == b;   // true(常量池复用)
String c = new String("abc");   // 新对象
c.intern();   // 返回池中引用
#

67. 时区(Zone)与 UTC 偏移(Offset)的概念差异

时区(Zone)与 UTC 偏移(Offset)的概念差异是什么?

  • Zone(时区)
  • Offset(偏移)
  • 概念差异

Zone(时区,ZoneId)与 Offset(UTC 偏移,ZoneOffset)概念差异:Offset 是"UTC 的固定偏移"(如 +08:00、-05:00),表示与 UTC 的固定时差,不含夏令时规则;Zone 是"时区"(如 Asia/Shanghai、America/New_York),包含偏移规则 + 夏令时(DST)规则,同一时区在不同季节偏移可能不同(夏令时)。差异:Offset 固定(无 DST),Zone 动态(含 DST 规则,偏移随季节变化)。java.time:ZoneOffset 表示固定偏移,ZoneId 表示时区(可查偏移)。Example:ZoneOffset.of("+08:00") 固定;ZoneId.of("America/New_York") 含夏令时。选择:固定偏移用 Offset 相关,完整时区用 Zone 相关。

Offset 是固定 UTC 偏移(无 DST),Zone 是时区(含 DST 规则,偏移随季节变化),ZoneOffset 固定、ZoneId 动态。

ZoneOffset offset = ZoneOffset.of("+08:00");      // 固定偏移(无 DST)
ZoneId zone = ZoneId.of("America/New_York");      // 时区(含 DST)
OffsetDateTime odt = OffsetDateTime.now(offset);
ZonedDateTime zdt = ZonedDateTime.now(zone);
#

68. 枚举的序列化为何只写出常量名字而不写字段,反序列化如何保证单例语义且不触发私有构造器

枚举的序列化为何只写出常量名字而不写字段?反序列化如何保证单例语义且不触发私有构造器?

  • 枚举序列化
  • 常量名
  • 单例语义

枚举的序列化特殊处理:只序列化"常量名字"(enum name),不序列化字段(枚举字段不参与序列化)。原因:Java 对枚举序列化有特殊机制,序列化时写枚举名,反序列化时用 Enum.valueOf(Class, name) 按名字解析到已有的枚举常量,返回单例实例。因此:1)反序列化不触发私有构造器(直接返回已有的枚举常量,不 new);2)保证单例语义(反序列化得到与现有常量相同的实例);3)字段不序列化(枚举字段不持久化,反序列化后为默认值)。机制:枚举序列化/反序列化走特殊路径(writeObject 写 name,readObject 用 valueOf),绕过构造器,保持单例。

枚举序列化只写常量名,反序列化用 Enum.valueOf 按名解析返回已有单例,不触发构造器、不写字段、保持单例。

enum Status { ACTIVE, INACTIVE }
// 序列化:只写 "ACTIVE"
// 反序列化:Enum.valueOf(Status.class, "ACTIVE") 返回单例
Status s = Status.ACTIVE;   // 反序列化后 s == Status.ACTIVE
#

69. 正则回溯导致的 ReDoS 攻击与防御

正则回溯导致的 ReDoS 攻击与防御是什么?

  • ReDoS
  • 正则回溯
  • 防御

ReDoS(正则表达式拒绝服务攻击):正则匹配的"回溯"(backtracking)在特定模式(嵌套量词、贪婪量词、复杂分支)下可能指数级回溯,匹配时间随输入长度指数增长,攻击者构造特殊输入使匹配卡死,耗尽 CPU(DoS)。防御:1)避免嵌套/重复量词(..、([a-z]+)+);2)用非贪婪量词、原子组((?>...))、占有量词(*+);3)限制输入长度(拒绝超长输入);4)设置正则匹配超时(某些库支持);5)用简单正则/预编译并测试性能;6)避免用户输入直接作为正则模式(模式注入)。ReDoS 防御核心是避免指数级回溯与超时。

正则回溯(嵌套量词)可能指数级匹配致 ReDoS,避免嵌套量词、用原子组、限制输入长度、超时防御。

// 危险:嵌套量词 (a+)+ 可能指数回溯
Pattern p = Pattern.compile("(a+)+$");   // 危险
// 防御:原子组
Pattern p2 = Pattern.compile("(?>a+)+$");  // 原子,避免回溯
// 限制输入长度
if (input.length() > 100) reject();
#

70. 正则表达式元字符、贪婪与非贪婪匹配

正则表达式元字符、贪婪与非贪婪匹配是什么?

  • 元字符
  • 贪婪/非贪婪
  • 匹配

正则元字符:.(任意字符)、\d(数字)、\w(单词字符)、\s(空白)、(0 次+)、+(1 次+)、?(0/1 次)、[abc](字符类)、^(行首)、$(行尾)、()(分组)等。贪婪与非贪婪:贪婪量词(、+、?)默认尽可能多匹配(贪婪),非贪婪(?、+?、??)尽可能少匹配(懒)。差异:贪婪匹配尽量多,遇到回溯减少;非贪婪尽量少,满足即可。示例:对 "aab" 匹配 a. 贪婪匹配 "aab",a.? 非贪婪匹配 "a"。选择:默认贪婪( 匹配尽量多),需要最短匹配用非贪婪(*?)。理解贪婪/非贪婪避免匹配错误。

元字符构成正则语法,贪婪量词尽量多匹配、非贪婪(*?)尽量少匹配,按需选择避免匹配范围错误。

Pattern p = Pattern.compile("a.*b");   // 贪婪:匹配 "aab" 整个
Pattern p2 = Pattern.compile("a.*?b");  // 非贪婪:匹配 "aab" 中 "ab"(最短)
// 输入 "aab",a.* 匹配 "aab",a.*? 匹配 "ab"
#

71. JDK 25 中 DateTimeFormatter 对 Unicode 区域数据(CLDR)的最新版本支持

JDK 25 中 DateTimeFormatter 对 Unicode 区域数据(CLDR)的最新版本支持是什么?

  • DateTimeFormatter
  • CLDR 数据
  • JDK 25

DateTimeFormatter 的格式/本地化依赖 CLDR(Unicode Common Locale Data Repository,区域数据)。JDK 25 更新了 CLDR 版本(随 JDK 升级),使 DateTimeFormatter 的本地化格式(ofPattern、预设格式、locale 相关)使用更新版本的 CLDR 数据,改善区域敏感格式(月份、星期、日期时间格式、区域名称)。支持内容:DateTimeFormatter 用 CLDR 提供本地化模式与数据,JDK 25 更新的 CLDR 版本提供更准确的区域数据。影响:日期格式(如 locale 的月份/星期名、标准格式)随 CLDR 更新而变化,需注意格式输出。JDK 中 CLDR 是默认 locale 数据源(JDK 9+ 默认 CLDR)。

DateTimeFormatter 依赖 CLDR 区域数据,JDK 25 更新 CLDR 版本,改善本地化日期格式与区域数据。

// JDK 25 更新 CLDR,DateTimeFormatter 本地化格式用最新 CLDR 数据
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy 'M'MMM", Locale.CHINA);
// 月份/星期名、区域格式随 CLDR 更新
#

72. String 的紧凑字符串(Compact Strings)压缩率

String 的紧凑字符串(Compact Strings)压缩率是什么?

  • Compact Strings
  • 压缩率
  • 内存

String 的紧凑字符串(Compact Strings,JEP 254,JDK 9)用 byte[] + coder 存储:若字符都在 LATIN-1(0-255),用 1 字节/字符,否则用 UTF-16(2 字节/字符)。压缩率:对纯 ASCII/拉丁字符(LATIN-1)字符串,从原来的 2 字节/字符(UTF-16 char[])降到 1 字节/字符,内存节省约 50%(减少一半)。对含非 LATIN-1 字符(如中文)仍用 2 字节,无压缩。大量 ASCII 字符串(日志、标识符)场景显著节省内存。压缩率取决于字符分布:LATIN-1 字符越多压缩率越高(最多约 50%)。

Compact Strings 用 byte[]+coder,LATIN-1 字符 1 字节/字符,ASCII 字符串内存节省约 50%,非 LATIN-1 仍 2 字节。

String ascii = "hello";   // LATIN-1:1 字节/字符,压缩约 50%
String cn = "中文";        // UTF-16:2 字节/字符,无压缩
// 压缩率取决于字符分布
#

73. 序列化流的魔数与类描述符结构如何组织,重复写出同一对象引用时句柄(handle)机制怎样避免对象图被复制多份

序列化流的魔数与类描述符结构如何组织?重复写出同一对象引用时句柄(handle)机制怎样避免对象图被复制多份?

  • 序列化流魔数
  • 类描述符
  • handle 机制

Java 序列化流格式:开头是魔数(0xACED)与版本号(0x0005),之后是对象/类描述符等。类描述符(ClassDescriptor)包含类名、serialVersionUID、字段描述符(字段名、类型、标志)。结构组织:魔数+版本 → 对象/类描述符 → 字段/数据。句柄(handle)机制:序列化流维护一个"句柄表",每个对象第一次写出时分配句柄(引用编号),后续写到同一对象引用时,只写句柄引用(不重复序列化对象),避免对象图被复制多份(共享引用、防循环引用)。反序列化用句柄解析回对象。handle 机制保证对象图共享引用、无重复、支持循环引用。

序列化流以魔数+版本开头,含类描述符;handle 机制让同一对象引用只写一次句柄,避免对象图复制多份、支持循环引用。

// 序列化流:AC ED 00 05(魔数+版本)
// 类描述符:类名、serialVersionUID、字段
// handle:同一对象第一次写全量,之后写句柄引用
Map<String,Object> m = new HashMap<>();
m.put("a", m);   // 循环引用,handle 机制避免无限
#

74. Matcher.matches/find/lookingAt 的语义差异与使用场景

Matcher.matches/find/lookingAt 的语义差异与使用场景是什么?

  • matches
  • find
  • lookingAt

Matcher 三个匹配方法语义差异:1)matches():整个输入必须完全匹配正则(全匹配),用于"验证整个字符串是否符合";2)find():在输入中查找匹配的子串(部分匹配,可多次调用找下一个),用于"搜索/提取",不要求整个输入匹配;3)lookingAt():从输入开头匹配(前缀匹配,但不用整个输入匹配),用于"输入是否以某模式开头"。差异:matches 全匹配、find 任意子串、lookingAt 开头前缀。使用场景:验证用 matches(如校验手机号整串)、搜索/提取用 find、判断前缀用 lookingAt。默认 find 部分匹配,matches 全匹配,注意区分。

matches 全匹配(验证)、find 查找子串(搜索/提取)、lookingAt 开头前缀,按匹配需求选择。

Matcher m = Pattern.compile("\\d+").matcher("abc123");
m.matches();    // false(整串不全是数字)
m.find();       // true(找到 "123")
Matcher m2 = Pattern.compile("\\d+").matcher("123abc");
m2.lookingAt(); // true(开头是数字)