一、先搞懂:什么是 String 的不可变?
String 的不可变指的是:一旦一个 String 对象被创建,它内部的字符序列(底层是char[] value数组,Java 9 后改为byte[])就无法被修改。看似修改 String 的操作(如拼接、替换),其实都是创建了一个全新的 String 对象,原对象不会有任何变化。
简单代码示例验证不可变:
java
public class StringImmutableDemo {
public static void main(String[] args) {
String s1 = "abc";
String s2 = s1; // s2和s1指向常量池中的同一个"abc"对象
// 看似修改s1,实际是创建新对象
s1 = s1 + "d";
System.out.println(s1); // 输出 "abcd"(新对象)
System.out.println(s2); // 输出 "abc"(原对象未变)
System.out.println(s1 == s2); // false,指向不同对象
}
}
二、String 设计为不可变的核心原因
1. 安全性(最核心原因)
String 是 Java 中最常用的类,大量用于存储敏感信息(如用户名、密码、数据库连接串、网络请求参数等),不可变设计能从根本上避免这些数据被篡改:
- 防止参数篡改:比如方法传参时,若 String 可变,方法内部修改参数会导致外部原变量被篡改,引发难以排查的 bug;
- 类加载安全:JVM 类加载时会通过类名(String 类型)定位类文件,若 String 可变,可能被恶意修改类名,导致加载错误的类,引发安全漏洞;
- 哈希表安全:String 常作为 HashMap、HashSet 的 Key,若可变,修改后哈希值会变化,导致 Key 对应的 Value 无法被正确查找,甚至引发哈希表混乱。
2. 支持字符串常量池(提升性能、节省内存)
Java 为了优化内存,设计了字符串常量池(String Pool)(位于堆内存的元空间):相同的字符串字面量只会在常量池中创建一次,所有引用都指向这个对象。
- 不可变是常量池的前提:如果 String 可变,那么修改常量池中的一个 String 对象,会导致所有引用它的变量都被篡改,这显然不符合预期;
- 性能提升:常量池复用字符串对象,避免了频繁创建和销毁对象的开销,大幅提升内存利用率。
代码示例:常量池复用
java
public class StringPoolDemo {
public static void main(String[] args) {
// 两个字面量都指向常量池中的同一个"hello"对象
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true,引用同一个对象
// new String会创建新对象,不复用常量池(但内部value仍指向常量池)
String s3 = new String("hello");
System.out.println(s1 == s3); // false
}
}
3. 天生的线程安全
不可变对象的状态在创建后就固定不变,多线程并发访问时,不需要加锁(同步)就能保证数据一致性,避免了线程安全问题。
- 比如多个线程同时读取同一个 String 对象,不用担心其中一个线程修改它的值,省去了同步锁的开销,提升并发性能。
4. 哈希值缓存(提升哈希集合性能)
String 的hashCode()方法会缓存哈希值(String 类中有一个private int hash成员变量存储哈希值):
- 因为 String 不可变,哈希值计算一次后就不会变化,后续调用
hashCode()直接返回缓存值,无需重新计算; - 这对 HashMap、HashSet 等依赖哈希值的集合来说,能大幅提升查找效率。
源码佐证(Java 8):
java
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
// 存储字符的数组,被final修饰,且没有提供修改这个数组的方法
private final char value[];
// 缓存哈希值
private int hash; // Default to 0
// 构造方法(示例)
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
// hashCode方法:缓存哈希值
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
}
关键点:value数组被final修饰,且 String 类没有提供任何修改value的 public 方法(如 set、修改数组元素),确保了不可变性。
总结
Java 将 String 设计为不可变的核心原因可归纳为 3 点:
- 安全性:防止敏感数据被篡改,保障类加载、哈希表等核心功能的安全;
- 性能与内存优化:支撑字符串常量池复用,缓存哈希值提升集合操作效率;
- 线程安全:不可变特性让 String 天生支持多线程并发访问,无需同步锁。
补充:如果需要可变的字符串,Java 提供了StringBuilder(非线程安全)和StringBuffer(线程安全),它们的设计目的就是解决 String 拼接时创建大量临时对象的问题,本质是可变的字符序列。