Android面试-为什么 Java 中的 String 被设计成不可变?
一、从一个常见现象说起
在 Java 中,String 是被使用频率最高的类之一。很多初学者都会遇到这样一个问题:既然字符串经常需要拼接、修改,为什么 JDK 还要把 String 设计成不可变(immutable)的?
先看一段代码:
String s = "hello";
s = s + " world";
System.out.println(s); // hello world
看起来 s 被“修改”了,但实际上原来的 "hello" 对象并没有变化,只是变量 s 指向了一个新的字符串对象 "hello world"。这正是不可变的含义:对象一旦创建,其内部状态就不能再被改变。
那么,这种设计到底带来了哪些好处?下面从几个关键角度来分析。
二、String 不可变的实现原理
在 JDK 8 及之前,String 内部使用 char[] 存储字符;从 JDK 9 开始,为了节省内存,改为使用 byte[] 加编码标志(Compact Strings)。无论哪种实现,核心字段都被声明为 final:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final byte[] value;
private int hash; // 缓存哈希值
// ...
}
不可变主要由三点保证:
- 类被
final修饰:不能被继承,避免子类破坏不可变语义。 - 内部数组被
final修饰且不对外暴露:外部无法拿到数组引用直接修改内容。 - 所有“修改”方法都返回新对象:如
substring、concat、replace等,都不会改动原对象。
需要注意的是,final 修饰引用只保证引用本身不变,并不保证数组内容不变。真正保证内容不可变的关键在于:String 没有提供任何可以修改内部数组元素的方法,也没有把数组引用泄露出去。
三、字符串常量池与内存复用
Java 中有字符串常量池(String Pool)。当我们写下字面量时,JVM 会优先从常量池中查找是否已存在相同内容的字符串:
String a = "java";
String b = "java";
System.out.println(a == b); // true
两个变量指向的是同一个对象。这种复用之所以安全,前提就是字符串不可变。如果 String 可变,那么通过 a 修改内容后,b 也会受到影响,整个常量池的共享机制就会崩溃。
不可变让常量池可以放心地共享对象,从而节省大量内存,也减少了对象创建的开销。
四、哈希值可以安全缓存
String 是 HashMap、HashSet 等集合中最常用的键类型。哈希表的正常工作依赖于:对象作为键期间,其哈希值不能发生变化。
由于 String 不可变,它的哈希值在创建后就是固定的,因此可以在第一次计算后缓存起来:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
// 计算哈希
hash = h;
}
return h;
}
这样既保证了哈希值稳定,又避免了重复计算,提升了集合操作的性能。如果 String 可变,那么把它作为 HashMap 的 key 后一旦内容改变,哈希值就会变化,导致再也无法正确查找到该键值对。
五、线程安全与并发友好
不可变对象天生是线程安全的。多个线程可以同时读取同一个 String 对象,而不需要任何同步措施,因为没有任何线程能够修改它的状态。
这在并发场景下意义重大:
- 省去了加锁的开销,读取性能更高。
- 避免了因共享可变状态导致的数据竞争问题。
- 可以安全地在多线程之间传递和共享字符串。
可以说,String 的不可变设计是 Java 并发编程中“不可变对象”思想的典型范例。
六、安全性保障
字符串在 Java 中被广泛用于敏感场景,例如类名、方法名、文件路径、网络地址、数据库连接串、用户名密码等。
如果 String 可变,攻击者就可能在某些环节篡改字符串内容,从而绕过安全检查。例如:
- 类加载时使用的类名被篡改,可能加载恶意类。
- 文件路径被修改,可能访问到未授权文件。
- 网络连接地址被替换,可能导致请求被劫持。
不可变保证了这些关键字符串在传递过程中不会被悄悄修改,从而提升了系统的整体安全性。
七、不可变带来的代价
任何设计都有取舍,String 不可变也并非没有成本:
- 频繁拼接会产生大量临时对象:每次
+操作都会创建新字符串,可能带来 GC 压力。 - 需要额外的可变替代类:因此 JDK 提供了
StringBuilder和StringBuffer用于可变字符串操作。
所以在循环中拼接字符串时,推荐使用 StringBuilder:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();
编译器也会对简单的字符串拼接做优化,但循环场景下显式使用 StringBuilder 仍然是更好的选择。
八、总结
String 被设计成不可变,是综合了内存、性能、并发和安全等多方面因素的结果。可以归纳为以下几点:
| 设计原因 | 说明 |
|---|---|
| 常量池复用 | 相同字面量共享同一对象,节省内存 |
| 哈希值缓存 | 哈希值稳定,适合作为集合的键 |
| 线程安全 | 不可变对象天然并发安全,无需加锁 |
| 安全性 | 关键字符串不会被中途篡改 |
| 实现简单 | 语义清晰,减少出错可能 |
理解 String 不可变的背后逻辑,不仅能帮助我们写出更高效的代码,也能加深对 Java 设计思想的理解。当需要频繁修改字符串时,记得选择合适的可变替代类,做到扬长避短。
使用小程序,查看更多面试题目,随时随地刷题,刷题更方便!


浙公网安备 33010602011771号