面试必备:String 为什么是不可变的?
先搞懂基本概念
什么是不可变对象?
一旦创建,对象的状态(属性值)不能被改变。
String s = "hello";
s = "world"; // 这不是改变原对象,而是创建了新对象
表面上 s 变了,但 "hello" 这个字符串本身从未改变。
String 的底层结构
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// JDK 9 之后使用 byte[] 存储
private final byte[] value;
// 编码标识
private final byte coder;
// 缓存的 hash 值
private int hash;
// ...
}
JDK 8 vs JDK 9+
| 版本 | 存储结构 | 说明 |
|---|---|---|
| JDK 8 | char[] value | 每个字符占 2 字节 |
| JDK 9+ | byte[] value + coder | Latin-1 用 1 字节,节省内存 |
为什么 String 是不可变的?
1. class 层面:final 修饰
public final class String { ... }
final 修饰类,表示类不能被继承,防止子类修改行为。
2. 存储层面:value 是 final 的
private final byte[] value;
value 数组的引用不能改变,但这不意味着数组内容不可变?
关键点:虽然数组内容理论上可以修改,但 String 类没有提供任何修改 value 的公开方法。
3. 方法层面:不提供修改方法
// String 类中没有这些方法
// public void setChar(int index, char c) {}
// public void append(String s) {}
// ...
所有"修改"操作都返回新对象:
String s1 = "hello";
String s2 = s1.toUpperCase(); // 返回新对象 "HELLO"
s1 // 仍是 "hello",未被改变
s2 // 是新对象 "HELLO"
不可变性的好处
1. 线程安全
不可变对象天然线程安全,不需要同步:
String s = "hello";
// 多个线程可以安全地共享这个 String,不需要加锁
2. 安全性
// 数据库连接、文件路径、网络地址都依赖 String
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db");
// 如果 String 可变,恶意代码可能修改连接字符串,造成安全漏洞
3. 字符串常量池
String s1 = "hello"; // 在常量池中创建
String s2 = "hello"; // 复用已有的,不创建新对象
System.out.println(s1 == s2); // true,指向同一个对象
不可变性是字符串常量池工作的基础:
- 如果 String 可变,两个引用指向同一个对象,一个改了,另一个也变了
- 缓存共享就变得危险
4. 哈希缓存
// String 的 hashCode 被缓存了
private int hash;
// 因为不可变,所以可以放心缓存
public int hashCode() {
int h = hash;
return (h != 0) ? h : (hash = computeHashCode());
}
5. 作为 HashMap/HashSet 的 key
Map<String, Integer> map = new HashMap<>();
map.put("hello", 1);
// 如果 String 可变,修改后会导致:
// 1. 存入的桶位置和查询的桶位置不一致
// 2. 永远找不到刚才放进去的值
字符串常量池
创建字符串的两种方式
// 方式1:字面量(使用常量池)
String s1 = "hello";
String s2 = "hello";
s1 == s2 // true
// 方式2:new 对象(不使用常量池)
String s3 = new String("hello");
String s4 = new String("hello");
s3 == s4 // false
s3 == s1 // false
图解
常量池:
┌─────────┐
│ "hello" │ ← s1, s2 都指向这里
└─────────┘
堆内存:
┌─────────────────┐ ┌─────────────────┐
│ new String() │ │ new String() │
│ s3 → │ │ │ s4 → │ │
└─────────────────┘ └─────────────────┘
intern() 方法
String s1 = new String("hello"); // 堆中
String s2 = s1.intern(); // 尝试放入常量池,返回池中的引用
System.out.println(s1 == s2); // false
System.out.println(s2 == "hello"); // true
String、StringBuilder、StringBuffer 对比
| 类 | 可变性 | 线程安全 | 性能 |
|---|---|---|---|
| String | 不可变 | 安全 | 每次修改都创建新对象,较慢 |
| StringBuilder | 可变 | 不安全 | 直接修改,少量对象,最快 |
| StringBuffer | 可变 | synchronized | 有锁,较慢 |
使用场景
// 字符串不常变化 → String
String name = "张三";
// 单线程字符串拼接 → StringBuilder
StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append("world");
// 多线程字符串拼接 → StringBuffer
StringBuffer sb = new StringBuffer();
编译器的优化
// 这种情况,编译器会优化成 StringBuilder
String s = "a" + "b" + "c";
// 编译后相当于
String s = new StringBuilder().append("a").append("b").append("c").toString();
面试高频问题
Q1:String s = new String("hello") 创建了几个对象?
两个:
- 字符串常量池中的
"hello" - 堆内存中的
new String()对象
Q2:String 有 length() 方法,数组有 length 属性,为什么?
- 数组是语言层面的设计,
length是属性 - String 是类,
length()是方法(因为还有charAt()等配套方法)
Q3:能不能继承 String?为什么?
不能。因为 String 是 final 类,不允许被继承。
总结
String 不可变的原因:
final修饰 class 和 value 数组- 不提供修改方法
- 好处:线程安全、字符串常量池、哈希缓存、安全性
记住一句话:不可变是设计选择,不是语言限制,Java 8 的 String 就是这么设计的。