JJava 知识库
JAVA INTERVIEW

高频面试题

Java基础基础约 3 分钟

项目里经常用 String 作为缓存 Key、Map Key 和签名参数,为什么 String 设计成不可变?这对业务有什么实际价值?

参考回答约 3 分钟 · 口语表达
先说结论

这个问题我会先说项目结论:一句话回答:String 类被 final 修饰是为了禁止继承和篡改行为;String 对象不可变,是因为内部数据不对外暴露,所有看似修改的方法都会返回新对象。 需要区分两个概念:final class String 表示 String 不能被继承;

01

我会先交代项目背景和选型结论。从 final、常量池、哈希缓存和线程安全理解 String。一句话回答:String 类被 final 修饰是为了禁止继承和篡改行为;String 对象不可变,是因为内部数据不对外暴露,所有看似修改的方法都会返回新对象。 需要区分两个概念:final class String 表示 String 不能被继承;字符串不可变来自类的整体封装设计,并不只是因为某个字段加了 final。

02

具体落地时,我会沿着实际调用链来讲。沿着「字面量或构造输入 → 常量池查找 → 创建不可变对象 → 计算并缓存哈希 → 安全共享与返回」观察输入、状态与输出,这些阶段都可以从日志、指标或源码里验证。 字面量或构造输入 编译期字面量会进入运行时常量池,运行期输入则先以普通对象存在,两者的创建时机与对象数量不同。 常量池查找 intern 会查询字符串池并返回池中规范引用,但它不是通用去重工具,调用成本和池容量都要评估。 java.lang.String:value、coder、hash 字段及 equals/hashCode 实现。 java.lang.invoke.StringConcatFactory:JDK 9+ invokedynamic 字符串拼接策略。

03

参数和容量不能靠默认值,我会结合业务量来定。用 JFR 的 Object Allocation in New TLAB 对比循环加号与预估容量 StringBuilder;固定输入和预热轮次,确认分配下降而结果一致。

04

效果要用数据证明,线上问题也要能闭环。固定输入和基线 先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「10 万次数字拼接」为主基线,记录值应满足「记录总分配字节」;同时保存 对象分配速率、Young GC 次数与停顿,使后续变化能够回到同一时间轴比较。 某支付系统把可变字符缓冲区转换后的结果作为签名缓存键,后续又复用了缓冲区,导致日志中的原文与缓存命中不一致。排查时应确认真正进入 Map 的对象类型、创建时机和编码步骤;改为不可变 String 值并在边界固定字符集后,签名键才具有稳定语义。 在循环中使用加号造成分配风暴:对象分配速率:把循环拼接改为单个 Builder 并预估容量。

05

最后我会主动说明这个方案不适合什么场景。方案:更适合的场景:主要收益:代价与边界。 String:只读文本、少量拼接和 Map 键:不可变、可共享、API 通用:循环修改会产生中间对象。 StringBuilder:单线程局部大量拼接:可变且无同步开销:不能跨线程无保护共享。 StringBuffer:遗留接口要求共享可变缓冲区:方法级同步:锁粒度粗,通常不解决完整并发流程。