
👨💻程序员三明治 :个人主页
🔥 个人专栏 : 《设计模式精解》 《重学数据结构》
🤞先做到 再看见!
目录
-
- 一、核心原理:底层是如何锁死"不变性"的?
-
- [1. 底层存储的私有化](#1. 底层存储的私有化)
- [2. 类定义的不可继承性](#2. 类定义的不可继承性)
- [二、 为什么要这么设计?(不可变的好处)](#二、 为什么要这么设计?(不可变的好处))
-
- [黑客视角:String 真的"绝对"不可变吗?](#黑客视角:String 真的“绝对”不可变吗?)
- 三、架构实战:如何设计一个自定义的不可变类?
- 四、版本更新后
-
- [1. 颠覆认知的更新:JDK 9 的内存优化 (Compact Strings)](#1. 颠覆认知的更新:JDK 9 的内存优化 (Compact Strings))
- [2. "不可变"带来的副作用:为什么密码不该用 String?](#2. “不可变”带来的副作用:为什么密码不该用 String?)
- [3. 性能陷阱:编译器的"糖衣炮弹"](#3. 性能陷阱:编译器的“糖衣炮弹”)
- [4. 孪生兄弟的较量:StringBuilder vs StringBuffer](#4. 孪生兄弟的较量:StringBuilder vs StringBuffer)
一、核心原理:底层是如何锁死"不变性"的?
String 的不可变性并非由单一关键字决定,而是由类结构、修饰符与底层实现共同构建的"防线":
1. 底层存储的私有化
在 JDK 的实现中(以 JDK 8 为例),String 类内部维护了一个核心的字符数组:

java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// 核心存储:一个 final 修饰的字符数组
private final char value[];
// ... 其他代码
}
在 Java 8 及以前,String 内部使用 private final char[] value 存储;Java 9 之后优化为 private final byte[] value。
final** 修饰引用:** 意味着value数组一旦被初始化,就不能再指向其他数组。private** 修饰访问权限:** 外部类无法直接操作该数组。由于 String 类内部没有提供任何修改value数组内容的方法(如setValue),且所有看似修改字符串的操作(如replace、concat)本质上都是返回一个新字符串,因此原始数据得到了保护。
2. 类定义的不可继承性
String 类被声明为 final class。
关键点: 这样设计是为了防止由于"子类化"破坏规则。如果没有 final,开发者可以编写一个 String 的子类并重写方法,通过某些手段修改父类内部的数据。final 彻底封死了通过继承篡改行为的可能性。
二、 为什么要这么设计?(不可变的好处)
如果 String 是可变的,Java 的很多核心功能将无法实现:
- 字符串常量池(String Pool)的需求 Java 为了节省内存,会将字面量字符串缓存。如果有两个变量
s1 = "abc"和s2 = "abc",它们其实指向同一个对象。如果 String 是可变的,一旦修改了s1,s2也会被无意中改变,这会导致巨大的混乱。 - 安全性的保障 String 经常被用作参数,例如网络连接的 URL、文件路径、数据库连接名等。如果 String 是可变的,在调用过程中如果内容被恶意篡改,会带来严重的安全漏洞。
- 支持高效的 Hash 缓存 由于 String 不可变,它的 HashCode 在创建时就可以被缓存(计算一次后存储)。这使得 String 在作为
HashMap的 Key 时性能极高,因为不需要每次都重新计算哈希值。 - 天然的线程安全 由于状态不可改变,String 对象可以在多个线程间共享而无需加锁,完全不存在竞态条件。
黑客视角:String 真的"绝对"不可变吗?
答案是:有,通过反射(Reflection)。
虽然 value 数组是 private 的,但在 Java 强大的反射机制面前,访问权限形同虚设。我们可以暴力破解访问权限,直接修改数组内部的字符。
举例:
java
public class StringTestDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
//创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("修改前:s = " + s); //Hello World
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第6个字符
value[5] = '_';
System.out.println("反射修改后:s = " + s); //Hello_World
}
}
输出结果:
修改前:s = Hello World
反射修改后:s = Hello_World
三、架构实战:如何设计一个自定义的不可变类?
理解了 String 的原理,我们能不能自己设计一个不可变类呢?
假设我们需要设计一个 Student 类,包含一个引用类型成员 People 和一个基本类型包装类 Integer age。要将它设计成不可变的,你需要遵循五大原则:
设计原则
- 类声明为 final:防止被继承和篡改方法。
- 成员变量为 private final:确保只能赋值一次,且外部无法访问。
- 不提供 Setter 方法:这就是"只读"的核心。
- 构造器深拷贝(Deep Copy) :这是最容易被忽视的一点。如果成员变量本身是可变的(如
People类),你不能直接赋值,必须拷贝一份新的。 - Getter 返回深拷贝:同样,不要把内部的可变对象直接丢给外部,否则外部拿到引用后就能修改你的内部状态。
java
public final class Student {
private final People people;
private final Integer age;
public Student(People people, Integer age) {
// 假设 People 是可变的,需要进行深拷贝
// this.people = new People(people); // 如果需要深拷贝
this.people = people;
this.age = age;
}
public People getPeople() {
// 如果 People 是可变的,需要返回深拷贝
// return new People(people); // 如果需要深拷贝
return people;
}
public Integer getAge() {
return age;
}
}
四、版本更新后
1. 颠覆认知的更新:JDK 9 的内存优化 (Compact Strings)
你的文章中提到 String 内部是 char[]。这在 JDK 8 及以前是完全正确的。但在 JDK 9 之后,这里发生了一个巨大的底层重构。
- 扩展点: 在 JDK 8 中,
char占用 2 个字节(UTF-16)。如果我们只存储简单的英文或数字(Latin-1 字符集),每个字符的高 8 位都是 0,这实际上浪费了一半的内存。 JDK 9 之后 ,String 内部改为了byte[] value加上一个coder编码标识。- 如果是纯英文,用单字节存储(节省 50% 空间)。
- 如果有中文,才回退到双字节存储。
代码对比:
- Java
java
// JDK 8
private final char value[];
// JDK 9+
private final byte[] value;
private final byte coder; // 标识是 Latin-1 还是 UTF-16
- 价值: 展示你不仅懂原理,还紧跟 Java 版本的演进。
2. "不可变"带来的副作用:为什么密码不该用 String?
既然 String 不可变这么好,为什么在处理敏感数据 (如用户密码、身份证号)时,安全专家建议使用 char[] 或 byte[] 而不是 String?
- 扩展点:
- String 的问题: 因为它不可变,一旦创建就会驻留在内存(堆)中,直到垃圾回收器(GC)来清理它。你无法手动销毁它。如果不幸发生了内存转储(Heap Dump),黑客可以在内存快照中明文看到密码。
- 数组的优势: 如果用
char[],在使用完密码后,你可以立刻显式地擦除数据(例如Arrays.fill(password, '0')),从而降低敏感数据泄露的风险。
- 价值: 结合网络安全场景,提升文章的实战深度。
3. 性能陷阱:编译器的"糖衣炮弹"
很多新手知道 String 拼接慢,要用 StringBuilder。但你知道编译器其实在偷偷帮你优化吗?
- 扩展点:
- 场景 A:
String s = "Hello" + " " + "World";这并不会创建 3 个对象。编译器非常聪明,在编译阶段就会把它优化成一个常量"Hello World"。
- 场景 A:
场景 B: 在循环中使用 + 拼接。
- Java
plain
String s = "";
for(int i=0; i<100; i++) {
s = s + i; // 灾难现场
}
这里编译器虽然会把单次拼接优化为 StringBuilder,但因为在循环里,它会重复创建 100 个 StringBuilder 对象。这里必须手动在这个循环外创建 StringBuilder。
- 价值: 纠正"字符串拼接一定慢"的刻板印象,展示编译器优化 与运行时性能的区别。
4. 孪生兄弟的较量:StringBuilder vs StringBuffer
既然 String 不可变,那想要"可变"的时候怎么办?
- 扩展点:
- StringBuffer: 也就是 JDK 1.0 就有的老前辈。它是线程安全 的(所有方法都加了
synchronized),但因此效率较低。 - StringBuilder: JDK 1.5 引入的新秀。它是非线程安全的,没有锁的开销,效率最高。
- 架构思考: 在局部变量(方法内部)拼接字符串时,因为没有线程竞争,永远首选
StringBuilder。
- StringBuffer: 也就是 JDK 1.0 就有的老前辈。它是线程安全 的(所有方法都加了
- 价值: 完善知识体系,形成"不可变(String) -> 可变安全(StringBuffer) -> 可变高效(StringBuilder)"的完整闭环。
如果我的内容对你有帮助,请辛苦动动您的手指为我点赞,评论,收藏。感谢大家!!
