目录
- 什么是
String? String的不可变性String的创建方式- 字符串字面量
new关键字
String常用方法String、StringBuilder和StringBuffer的区别String的内存布局(常量池)- 最佳实践
什么是 String?
在 Java 中,String 表示一个字符串,它不是一个基本数据类型(如 int, char),而是一个对象,这个对象内部使用一个 final 的 char 数组来存储字符序列。

// String 类的内部结构简化版
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[]; // 核心:一个不可变的字符数组
// ... 其他成员变量和方法
}
从上面的定义可以看出 String 有两个重要特性:
final:String类不能被继承。value数组是final的:这个数组一旦被创建,其引用就不能再指向其他数组。
String 的不可变性
这是 String 最核心、最重要的特性。一旦一个 String 对象被创建,它的内容就不能被修改。
为什么需要不可变性?
- 线程安全不可变,
String对象在多线程环境下是安全的,无需额外的同步机制。 - 安全性:在 Java 中,
String被广泛用于加载类、文件路径、网络地址等,如果字符串内容可以被修改,可能会带来严重的安全风险(修改了加载类的路径)。 - 性能优化:由于不可变,Java 虚拟机可以对
String进行优化,比如字符串常量池。
如何“修改”字符串?

当你看起来在修改一个字符串时,JVM 会创建一个新的 String 对象,原来的对象保持不变。
String s1 = "hello";
String s2 = s1; // s2 和 s1 指向同一个对象
// s1.concat(" world") 并没有改变 s1 指向的对象
// 它返回了一个新的 "hello world" 字符串对象
s1 = s1.concat(" world");
System.out.println(s1); // 输出: hello world
System.out.println(s2); // 输出: hello (s2 仍然指向原来的 "hello" 对象)
String 的创建方式
Java 中创建字符串主要有两种方式,它们的内存行为完全不同。
字符串字面量
String s1 = "Hello"; String s2 = "Hello";
这种方式创建的字符串,JVM 会将其放入一个叫做字符串常量池 的全局内存区域中。
- 当执行
String s1 = "Hello";时,JVM 会在常量池中查找是否存在 "Hello"。 - 如果不存在,就创建一个 "Hello" 对象并存入常量池,然后将
s1引用指向它。 - 如果存在,
s1就直接指向这个已存在的对象。 s1 == s2的结果是true,因为它们指向同一个对象。
String s1 = "Hello"; String s2 = "Hello"; System.out.println(s1 == s2); // 输出: true (比较的是引用地址) System.out.println(s1.equals(s2)); // 输出: true (比较的是内容)
new 关键字
String s3 = new String("Hello");
String s4 = new String("Hello");
这种方式创建的字符串,总是在堆内存 中创建一个新的对象,而不会检查常量池。

new String("Hello")的过程是:- "Hello" 会被作为字面量放入常量池(如果还没有的话)。
new关键字会在堆内存中创建一个新的String对象,并将常量池中的 "Hello" 内容复制到这个新对象中。s3引用指向这个堆内存中的新对象。
s3 == s4 的结果是 false,因为它们是堆中两个不同的对象。
String s3 = new String("Hello");
String s4 = new String("Hello");
System.out.println(s3 == s4); // 输出: false (比较的是引用地址)
System.out.println(s3.equals(s4)); // 输出: true (比较的是内容)
String 常用方法
String 类提供了非常丰富的方法来操作字符串。
| 方法 | 描述 | 示例 |
|---|---|---|
length() |
返回字符串的长度。 | "abc".length() -> 3 |
charAt(int index) |
返回指定索引处的字符。 | "abc".charAt(1) -> 'b' |
substring(int beginIndex) |
返回从 beginIndex 开始到末尾的子字符串。 |
"abcde".substring(2) -> "cde" |
substring(int begin, int end) |
返回从 begin 到 end-1 的子字符串。 |
"abcde".substring(1, 3) -> "bc" |
indexOf(String str) |
返回子字符串第一次出现的索引,找不到返回 -1。 | "hello world".indexOf("world") -> 6 |
lastIndexOf(String str) |
返回子字符串最后一次出现的索引。 | "hello world".lastIndexOf("o") -> 7 |
toUpperCase() / toLowerCase() |
将字符串转换为大写/小写。 | "Hello".toLowerCase() -> "hello" |
trim() |
去除字符串首尾的空白字符。 | " hello ".trim() -> "hello" |
replace(char old, char new) |
替换所有 old 字符为 new 字符。 |
"hello".replace('l', 'p') -> "heppo" |
split(String regex) |
根据正则表达式分割字符串,返回字符串数组。 | "a,b,c".split(",") -> ["a", "b", "c"] |
equals(Object obj) |
比较两个字符串的内容是否相等(区分大小写)。 | "abc".equals("ABC") -> false |
equalsIgnoreCase(String another) |
比较两个字符串内容是否相等(不区分大小写)。 | "abc".equalsIgnoreCase("ABC") -> true |
compareTo(String another) |
按字典顺序比较两个字符串。 | "abc".compareTo("abd") -> -1 (因为 'c' < 'd') |
String、StringBuilder 和 StringBuffer 的区别
当需要进行大量的字符串拼接、修改操作时,由于 String 的不可变性,每次操作都会创建新对象,导致性能低下,这时应该使用 StringBuilder 或 StringBuffer。
| 特性 | String |
StringBuilder |
StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 线程安全(因为不可变) | 非线程安全 | 线程安全 |
| 性能 | 拼接等操作性能差 | 性能最高 | 性能比 StringBuilder 稍低 |
| 使用场景 | 少量、固定的字符串 | 单线程环境下的字符串操作 | 多线程环境下的字符串操作 |
示例:性能对比
// String 拼接 (性能差)
String s = "";
for (int i = 0; i < 10000; i++) {
s = s + "a"; // 每次循环都创建新对象
}
// StringBuilder 拼接 (性能好)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("a"); // 在原有对象上修改,效率高
}
String result = sb.toString();
String 的内存布局(常量池)
字符串常量池是 JVM 的一块特殊内存区域,用于存储所有字符串字面量,它的主要目的是复用字符串对象,减少内存开销。
JDK 7 的重要变化: 在 JDK 7 之前,字符串常量池位于永久代,从 JDK 7 开始,它被移到了堆内存中,这意味着字符串常量池中的对象可以被垃圾回收器回收。
示例:intern() 方法
intern() 方法是一个手动将字符串对象“入池”的方法。
String s1 = new String("Hello"); // "Hello" 在常量池,s1 在堆
String s2 = s1.intern(); // 将 s1 的内容 "Hello" 放入常量池,如果池中已有,则直接返回池中的引用
String s3 = "Hello"; // s3 直接从常量池获取
System.out.println(s1 == s2); // false, s1是堆对象,s2是常量池对象
System.out.println(s2 == s3); // true, s2和s3都指向常量池中的 "Hello"
最佳实践
- 优先使用字面量:对于已经确定且不会改变的字符串,直接使用字面量(
String s = "hello";),让 JVM 自动管理常量池。 - 避免不必要的 拼接:在循环或频繁修改字符串的场景中,绝对不要使用 或 来拼接字符串,这会导致大量临时对象的创建,造成性能问题,务必使用
StringBuilder或StringBuffer。 - 使用
equals()而不是 比较内容: 比较的是两个对象的内存地址,而equals()比较的是对象的内容,绝大多数情况下,我们关心的是内容是否相同。 - 明确线程需求:如果确定代码只在单线程中运行,优先使用性能更高的
StringBuilder,如果涉及多线程共享和修改,则必须使用StringBuffer。
希望这份详细的讲解能帮助你全面理解 Java 中的 String!
