String不可变是JVM在安全、性能与内存管理间权衡的刚性设计:保障字符串常量池安全复用、哈希值可缓存、类加载与敏感参数不被篡改,且由final类、private final数组及无写入接口三重硬约束实现。

String 不可变不是为了“显得高级”,而是 JVM 在安全、性能和内存管理之间权衡出的刚性设计。一旦接受这个前提,很多看似反直觉的行为(比如 s = s + "a" 生成新对象)就自然了。
字符串常量池依赖不可变性才能安全复用
常量池不是“优化开关”,而是不可变性的必然产物。JVM 把字面量如 "abc" 放进常量池,后续所有同内容字面量都复用同一对象——前提是没人能改它。
- 如果
s1 = "abc"和s2 = "abc"共享同一对象,但某处调用s1.setCharAt(0, 'x')(假设存在),s2就会变成"xbc",逻辑彻底崩坏 -
new String("abc")强制在堆上新建对象,绕过常量池;而"abc".intern()是手动“拉回”池中,但 JDK 7+ 后池在堆里,查表开销仍存在,别在循环里滥用 - 常量池复用效果明显:日志模板、HTTP 状态码(
"200 OK")、配置键名("database.url")这类高频重复字符串,堆内存占用可降低数倍
哈希值缓存必须基于内容永不变化
String 的 hashCode() 方法内部有 private int hash 字段,首次调用才计算并缓存。这个优化只在不可变前提下成立。
- 若字符串可变,
hash缓存瞬间失效,每次map.get(key)都得重新遍历字符数组算一遍,时间复杂度从O(1)退化为O(n) - 更严重的是语义错误:把
"user"当 key 存进HashMap,之后又把它改成"USER",hashCode()变了,但 map 还按旧桶找——map.get("user")直接返回null,数据“消失”了 - 注意:
StringBuilder.toString()返回的新String对象,即使内容相同,也是独立对象,hashCode()会正常缓存,没问题;但若用反射强行改了底层value数组,就破坏了整个契约
安全场景下不允许任何意外修改
字符串大量用于敏感路径:类名、文件路径、数据库 URL、HTTP 请求头。不可变性是这些场景不被篡改的底线保障。
- 类加载器靠
String表示类名,若可变,双亲委派可能被绕过;反射调用clazz.getMethod("doSomething"),方法名被改掉就直接抛NoSuchMethodException - 典型漏洞场景:
void connectToDatabase(String url)先校验url.startsWith("jdbc:mysql://"),再传给驱动。若url可变,校验后另一线程把它改成"jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3",就可能触发恶意 H2 数据库内存执行 - 安全检查与实际使用之间存在时间窗口,不可变对象把这个窗口的影响降为零——校验时是什么,执行时还是什么
真正容易被忽略的点在于:不可变性不是靠文档约定,而是由 private final byte[] value(JDK 9+)、final 类修饰、无写入接口三重硬约束实现的。哪怕你用反射绕过访问控制去改 value,也会破坏常量池一致性,属于未定义行为,JVM 不保证后续表现。










