String 是一个引用类型,但它有一些特殊的待遇。
这句话包含了两层意思:

- 本质是引用类型:和
Object,Array,List一样,String变量存储的是对象在内存中的地址(引用),而不是对象本身的数据。 - 有特殊的语法和优化:Java 为
String提供了特殊的语法糖(如字面量)和内存优化机制(字符串常量池),这使得它的使用方式看起来和基本类型很像。
String 作为引用类型的工作原理
让我们从最基础的开始。
a. 对象的创建
当你创建一个 String 对象时,实际上是在堆内存中分配了一块空间来存储字符序列,而变量则指向这块空间的地址。
使用 new 关键字(显式创建)
String str1 = new String("Hello");
new String("Hello"):这部分在堆内存中创建了一个String对象,内容为 "Hello"。str1:这是一个引用变量(可以理解为指针),它存储了上面创建的那个对象的内存地址。
使用字面量(隐式创建)

String str2 = "Hello";
"Hello":这是一个字符串字面量,当 Java 虚拟机在编译或运行时遇到这种字面量,它会去一个特殊的内存区域——字符串常量池——查找是否已经存在 "Hello" 这个字符串。- 如果不存在,就在常量池中创建一个新的 "Hello" 字符串,并将
str2引用指向它。 - 如果已经存在,就直接将
str2引用指向常量池中已有的那个 "Hello" 字符串。
- 如果不存在,就在常量池中创建一个新的 "Hello" 字符串,并将
无论是 new 还是字面量,str1 和 str2 这两个变量本身都只是引用,它们指向了真正存放 "Hello" 数据的对象。
b. 引用赋值与“指向”
引用类型的经典特性就是多个变量可以指向同一个对象。
String a = "hello"; // 在常量池中创建 "hello",a 指向它
String b = a; // b 被赋值为 a 的引用,b 也指向同一个 "hello" 对象
// 现在修改 a 指向一个新的对象
a = a + " world"; // 1. 创建一个新的字符串 "hello world" (可能在堆或常量池)
// 2. 让 a 澄清这个新对象
// 3. b 仍然指向原来的 "hello"
System.out.println(a); // 输出: hello world
System.out.println(b); // 输出: hello
这个例子清晰地表明,a 和 b 是独立的引用,它们可以指向不同的对象。a = a + " world" 并没有修改 a 原来指向的那个对象,而是让 a 指向了一个全新的对象。
String 的特殊性:不可变性与字符串常量池
String 最大的特殊性在于它的不可变性。

a. 不可变性
一旦一个 String 对象被创建,它的内容(内部的字符数组 char[] value)就无法被修改。
任何看起来像是在修改 String 的操作(如 concat(), replace(), 运算符),实际上都是:
- 基于原始字符串创建一个新的
String对象。 - 将修改后的内容放入这个新对象中。
- 返回这个新对象的引用。
示例:
String s = "original";
s = s.concat(" modified"); // concat() 方法返回了一个新的字符串对象
// s 现在指向了新的 "original modified" 对象
// 原来的 "original" 对象如果没有任何引用指向它,就会被垃圾回收
不可变性的好处:
- 线程安全不能被改变,所以多个线程可以同时访问一个
String对象而无需同步,不会发生数据冲突。 - 安全:在处理文件路径、网络地址、数据库用户名密码等敏感信息时,不可变性确保了它们在创建后不会被恶意修改。
- 性能优化(字符串常量池的基础)不变,JVM 可以放心地将字符串字面量放入一个公共的“池子”中,供所有代码共享,避免了重复创建相同内容的字符串,从而节省内存。
b. 字符串常量池
这是一个位于方法区(或元空间)的特殊内存区域,专门用来存储字符串字面量和 intern() 方法返回的字符串引用。
工作流程:
- 当编译器遇到
String s = "abc";这样的代码时,它会检查常量池。 - "abc" 不在池中,就将其加入池中,并将
s的引用指向池中的 "abc"。 - "abc
已经在池中,就直接将s` 的引用指向池中已有的 "abc"。
new String() vs. 字面量的区别:
String s1 = "Hello"; // 1. 检查常量池,"Hello" 不存在,则创建并放入池中,s1 指向池中的 "Hello"。
String s2 = "Hello"; // 2. 检查常量池,"Hello" 已存在,s2 直接指向池中的同一个 "Hello"。
String s3 = new String("Hello"); // 3. 总是在堆内存中创建一个新的 "Hello" 对象,然后检查常量池,"Hello" 不存在,则将其放入池中(但 s3 仍然指向堆中的对象)。
System.out.println(s1 == s2); // true (比较引用,指向同一个池中对象)
System.out.println(s1 == s3); // false (比较引用,s1指向池中,s3指向堆中)
String、StringBuilder 和 StringBuffer
由于 String 的不可变性,频繁进行字符串拼接(如在循环中)会创建大量临时对象,导致性能下降和内存浪费,为了解决这个问题,Java 提供了两个可变的工具类。
| 特性 | String |
StringBuilder |
StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全 (因为不可变) | 不安全 | 安全 (方法有 synchronized 关键字) |
| 性能 | 拼接性能差 | 性能最好 (无同步开销) | 性能比 StringBuilder 差 (有同步开销) |
| 使用场景 | 固定不变的字符串内容 | 单线程环境下的字符串操作,如循环拼接 | 多线程环境下的字符串操作 |
示例:
// 高效的单线程拼接
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("i"); // 在同一个对象上进行修改,不创建新对象
}
String result = sb.toString();
- 引用类型本质:
String变量存储的是对象的引用(地址),而不是字符序列本身。String s = "abc";中,s是引用,"abc"是对象。 - 不可变性:
String对象一旦创建,内容就不能被修改,任何修改操作都会返回一个新的String对象。 - 字符串常量池:为了优化性能和节省内存,JVM 维护了一个字符串常量池,使用字面量创建的字符串会被放入池中,实现复用。
new String()总是在堆上创建新对象。 - vs.
equals():- 比较两个引用是否指向同一个对象(内存地址是否相同)。
equals():String重写了equals()方法,它比较的是两个字符串的是否相同。
- 性能考量:对于需要频繁修改的字符串,在单线程环境下应优先使用
StringBuilder,在多线程环境下使用StringBuffer。
理解了 String 作为引用类型的这些特性和优化机制,你就能写出更高效、更健壮的 Java 代码。
