String不可变性由final类、private final value数组、无修改方法共同实现:类不可继承,数组引用不可重赋且外部不可访问,所有操作均返回新对象,保障线程安全、哈希缓存与常量池正确性。

String不可变性不是一句“final修饰”就能讲透的,面试进阶时考的是你能否从源码结构、设计约束、运行时行为和实际影响四个层面串联起来。关键不在于背结论,而在于能指出哪几行代码、哪几个修饰符、哪几类方法共同构成了“不可变”这个事实。
看类声明和字段:final + private + 无公开修改入口
打开JDK 17(或JDK 9+)源码,核心就三行:
public final class String —— 类不能被继承,杜绝子类绕过封装;
private final byte[] value —— 数组引用不可重赋值,且外部拿不到引用;
private final byte coder —— 编码标识也私有不可改。
注意:final修饰的是数组引用,不是数组元素本身。真正锁死修改路径的,是private——没有getter返回value数组,也没有任何public方法暴露索引写入能力。所有看似“修改”的方法(如concat、replace、substring)都返回新String对象,内部调用的是new String(...)或StringLatin1.newString(...)这类构造逻辑。
盯住构造器和关键方法:只读语义贯穿始终
以JDK 17中的substring(int beginIndex, int endIndex)为例:
立即学习“Java免费学习笔记(深入)”;
- 它不复用原value数组,而是调用
String(byte[], int, int, byte)构造器,拷贝对应范围的字节; - JDK 9之前用
Arrays.copyOfRange(value, begin, end),JDK 9之后根据coder判断走StringLatin1.newString或StringUTF16.newString; - 哪怕只是取一个子串,原字符串的value数组也不会被共享引用——避免外部通过反射或unsafe篡改影响其他实例。
再看hashCode():hash字段是普通int,但它的计算逻辑被严格限制在第一次调用时执行,并缓存结果。之所以敢缓存,前提就是value和coder从不变化。如果某个方法偷偷改了value内容,缓存就失效甚至出错——而源码里根本找不到这样的方法。
结合常量池和线程场景:不可变是安全的基础设施
面试官常追问“为什么非得不可变”,答案要落到JVM机制和并发模型上:
- 字符串字面量如
"abc"会进入字符串常量池,多个变量可能共用同一对象。若可变,s1.replace('a', 'x')会静默改变s2看到的内容; - 作为HashMap的key时,hashCode和equals结果必须稳定。String的hashCode缓存+不可变字符序列,保证了key放入后散列位置永不漂移;
- 多线程环境下直接传递String参数(如文件路径、SQL模板、配置项),无需同步或防御性拷贝——因为没人能动它。
这些不是“顺便支持”的特性,而是String被设计成不可变的直接动因。源码里没有一行写着“为了线程安全”,但每一处private final、每一个无副作用的方法签名,都在为这些场景兜底。
点破边界情况:反射和Unsafe也不能真正破坏契约
有经验的面试官会问:“那用反射修改value数组呢?”这时要分两层答:
- 技术上可行:通过
Field.setAccessible(true)确实能拿到value并修改字节数组内容; - 但逻辑上已脱离Java规范:这种操作绕过了类的设计契约,破坏了String作为“不可变值对象”的语义。JVM不保证这种行为下的hashCode、equals、intern等行为仍正确;
- 真实项目中,SecurityManager或模块化系统(如JPMS)会默认阻止此类反射,生产环境基本不可行。
所以源码级的不可变,是靠“语法约束(final/private)+ 行为约束(无修改API)+ 运行约束(JVM保障)”三层共同实现的,不是单点防护。


















