问题
String是如何实现不可变的?
答案
1. 核心实现机制
String通过四个关键设计手段保证不可变性:
- 类声明为final - 防止子类破坏不可变性
- 内部数组使用final - 保证引用不可变
- 私有化内部状态 - 外部无法直接访问
- 所有方法返回新对象 - 不修改原对象
2. 源码剖析
JDK 8 实现
public final class String // ① final类,不能被继承
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; // ② final + private,内容不可变
/** Cache the hash code for the string */
private int hash; // Default to 0 // ③ hashCode缓存
// ④ 构造方法进行防御性拷贝
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length); // 拷贝数组,而非直接引用
}
// ⑤ 所有"修改"方法都返回新对象
public String substring(int beginIndex, int endIndex) {
// ...
return new String(value, beginIndex, subLen); // 返回新对象
}
public String concat(String str) {
// ...
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true); // 返回新对象
}
public String replace(char oldChar, char newChar) {
// ...
char buf[] = new char[len];
// ... 填充buf
return new String(buf, true); // 返回新对象
}
}
JDK 9+ 实现(Compact Strings优化)
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value; // 改用byte[],节省内存
private final byte coder; // 编码标识:LATIN1 或 UTF16
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
// 根据字符集选择编码
public String(char value[], int offset, int count) {
if (COMPACT_STRINGS) {
byte[] val = StringUTF16.compress(value, offset, count);
if (val != null) {
this.value = val;
this.coder = LATIN1; // 使用单字节编码
return;
}
}
this.coder = UTF16; // 使用双字节编码
this.value = StringUTF16.toBytes(value, offset, count);
}
}
3. 四大实现技术详解
技术1:final类声明
public final class String {
// ...
}
作用:
- 防止被继承,避免子类重写方法破坏不可变性
假设String不是final:
// 假设String可以被继承
class MutableString extends String {
private char[] mutableValue;
@Override
public char charAt(int index) {
return mutableValue[index]; // 返回可变数组的内容
}
public void setCharAt(int index, char c) {
mutableValue[index] = c; // 破坏不可变性!
}
}
// 问题:多态调用时无法保证不可变性
String str = new MutableString("hello");
((MutableString) str).setCharAt(0, 'H'); // 修改成功!
技术2:final字段
private final char[] value; // JDK 8
private final byte[] value; // JDK 9+
作用:
final保证value引用不可变(不能指向新数组)private保证外部无法直接访问
重要区别:
// final保证引用不变,但数组内容理论上可变
private final char[] value = {'h', 'e', 'l', 'l', 'o'};
// 以下操作是非法的(编译错误)
value = new char[]{'w', 'o', 'r', 'l', 'd'}; // ❌ final引用不能修改
// 以下操作理论上可行(如果能访问value的话)
value[0] = 'H'; // ⚠️ 数组内容可变,但String通过private和无修改方法防止了这一点
String通过不暴露修改数组的方法来保证数组内容不可变。
技术3:防御性拷贝
// 构造方法:拷贝传入的数组
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length); // 深拷贝
}
// 错误示例:直接引用(假设)
public String(char value[]) {
this.value = value; // ❌ 外部可以修改数组
}
防止外部修改:
char[] chars = {'h', 'e', 'l', 'l', 'o'};
String str = new String(chars);
// 如果没有防御性拷贝,外部修改会影响String
chars[0] = 'H'; // String内部拷贝了数组,不受影响
System.out.println(str); // 输出:hello(而非Hello)
返回值的防御性处理:
// String的toCharArray方法也进行拷贝
public char[] toCharArray() {
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result; // 返回新数组,防止外部修改内部状态
}
技术4:返回新对象而非修改原对象
// substring:返回新String
public String substring(int beginIndex) {
// ...
return new String(value, beginIndex, subLen);
}
// concat:返回新String
public String concat(String str) {
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
// replace:返回新String
public String replace(char oldChar, char newChar) {
// ... 查找替换逻辑
return new String(buf, true);
}
// toLowerCase:返回新String
public String toLowerCase() {
// ...
return new String(result, 0, len + resultOffset);
}
关键点:
- 所有看似”修改”的方法都创建新对象
- 原对象保持完全不变
- 符合不可变对象模式的设计原则
4. 反射能否破坏不可变性?
理论上可以通过反射修改:
String str = "hello";
System.out.println(str); // hello
// 通过反射获取value字段
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
// JDK 8
char[] value = (char[]) valueField.get(str);
value[0] = 'H';
// JDK 9+
byte[] value = (byte[]) valueField.get(str);
value[0] = 'H';
System.out.println(str); // Hello - 不可变性被破坏!
警告:
- 这种做法极不推荐,会导致不可预测的行为
- 可能影响字符串常量池中的所有引用
- 破坏hashCode缓存,导致HashMap等数据结构失效
字符串常量池问题:
String s1 = "hello";
String s2 = "hello"; // 指向同一对象
// 反射修改s1
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(s1);
value[0] = 'H';
System.out.println(s1); // Hello
System.out.println(s2); // Hello - s2也被修改了!
5. 不可变性的完整保证
// 不可变类的标准实现模式
public final class String { // ① final类
private final char[] value; // ② final + private字段
public String(char[] value) { // ③ 防御性拷贝
this.value = Arrays.copyOf(value, value.length);
}
public char[] toCharArray() { // ④ 返回拷贝
return Arrays.copyOf(value, value.length);
}
public String concat(String str) { // ⑤ 返回新对象
// ...
return new String(buf, true);
}
// ⑥ 没有提供任何修改内部状态的公开方法
// 没有setValue()、setCharAt()等方法
}
6. 不可变类设计最佳实践
基于String的实现,设计不可变类的通用模式:
public final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies;
public ImmutablePerson(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
// 防御性拷贝
this.hobbies = new ArrayList<>(hobbies);
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public List<String> getHobbies() {
// 返回不可修改的视图
return Collections.unmodifiableList(hobbies);
}
// "修改"方法返回新对象
public ImmutablePerson withAge(int newAge) {
return new ImmutablePerson(this.name, newAge, this.hobbies);
}
}
7. 面试答题要点
标准回答结构:
- final类:防止继承破坏不可变性
- final字段:内部char[]/byte[]使用final修饰,保证引用不变
- 私有字段:private修饰,外部无法直接访问
- 防御性拷贝:构造方法拷贝传入的数组,返回方法拷贝内部数组
- 返回新对象:所有”修改”方法(substring、concat等)都返回新String对象
- 无修改方法:不提供任何修改内部状态的公开方法
加分点:
- 说明JDK 9的byte[]优化(Compact Strings)
- 提到反射可以破坏不可变性,但不推荐
- 了解不可变对象模式的设计原则
- 能够设计自己的不可变类
8. 总结
String的不可变性是通过语言级别(final)、封装(private)、拷贝(防御性)和设计(无修改方法)的多层保障实现的:
| 层次 | 技术手段 | 保障内容 |
|---|---|---|
| 类级别 | final class | 防止继承 |
| 字段级别 | final + private | 引用不变 + 不可访问 |
| 构造级别 | 防御性拷贝 | 防止外部修改 |
| 方法级别 | 返回新对象 | 不修改原对象 |
| API级别 | 无修改方法 | 无破坏接口 |
这种纵深防御的设计思想,使String成为Java中最可靠的不可变类,也是不可变对象模式的典范实现。