结论先行:String 的不可变,本质是
String内部用来存字符的数组是 private final,并且 String 没有提供任何修改这个数组的对外方法;一旦创建,字符内容无法在原地修改。 JDK9 做了优化:底层从char[]改成了byte[](加上一个 coder 标记是 LATIN1 还是 UTF16),但不可变设计思想完全没变。
一、JDK8 源码(经典 char [] 版本)
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
// ...
}
关键点拆解:
private char value[]:数组私有,外部拿不到这个引用,不能直接操作数组元素;final char value[]:final 修饰的是数组引用,不是数组里面的元素!⚠️ 重点坑:
final char[] value只保证value不能指向另一个数组,不代表数组内部元素不能改。 所以光靠 final 不够,还要配合:String 类内部不对外暴露这个数组、不提供修改数组元素的方法;String类本身是final:不能被继承,防止子类重写方法破坏不可变特性。
简单一句话:final 锁引用 + private 封装 + 没有任何修改 value 的 API,三者一起实现不可变。
二、JDK9+ 底层变化(byte [] + coder)
private final byte[] value;
private final byte coder; // 0 = LATIN1,1 = UTF16
目的:节省内存。如果字符串只包含单字节字符,用单字节存储;中文等用 UTF16。 不可变规则不变:依然 private final,没有对外修改手段。
三、那为什么 s = s + "abc" 看起来 “修改了字符串”?
String s = "hello";
s = s + " world";
✅ 不是修改原来的 hello 对象,而是:
在堆 / 常量池创建一个全新的 String 对象,内容是
hello world;变量
s这个引用,指向新对象;原来
"hello"的 String 对象还在,内容丝毫没变,等待 GC。
所有 String 方法:substring / replace / trim / toUpperCase 都不会修改原字符串,返回新 String。
JDK6 的 substring 有内存泄漏坑:共享原 char [];JDK7 之后 substring 会新建 char [],不再共享。
四、能不能绕过,强行修改 String?
可以用反射(不推荐,破坏封装,高版本有限制):
String s = "abcd";
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
char[] arr = (char[]) valueField.get(s);
arr[0] = 'x';
System.out.println(s); // xbcd
👉 这属于破坏不可变约定,不是正常用法;而且:
字符串常量池里的字面量,反射修改会引发诡异问题(多处共用同一个字符串);
高版本 JDK(17 + 模块化)会禁止访问 String 的内部字段。
五、String 设计成不可变的好处
字符串常量池(String Pool) 多个引用共用池里同一个字符串对象,不用担心别人改了我的字符串,池化才有安全基础。
作为 HashMap / HashSet 的 key
hashCode()只计算一次,缓存起来(private int hash;);不可变保证 hash 值永远不变,不会丢 key。线程安全 多线程同时读同一个 String,没有并发修改问题,不需要同步。
安全 比如网络连接参数、文件路径、权限参数,作为不可变对象,不容易被中途篡改。
六、对比:StringBuilder / StringBuffer 为什么可变?
底层也是数组,但:
数组不是 private final(或者说会主动扩容、修改内部元素)
提供
append、delete、replace等方法直接修改内部数组,不每次新建对象适合频繁字符串拼接;
StringBuffer加了 synchronized,线程安全,性能差一点。
七、一句话总结
String 不可变不是因为 final 数组元素不能改,而是:底层字符数组 private 私有、没有对外提供修改数组的方法、String 类 final 不可继承;所有修改操作都会生成新对象。final 只是保证 value 引用不能重新指向别的数组。