目录
String的基本概念String的不可变性String的创建方式String的常用方法String、StringBuilder和StringBuffer的区别String的内存布局与intern()方法- 最佳实践
String 的基本概念
在 Java 中,String (字符串) 是一个对象,而不是基本数据类型,它代表一个字符序列,"Hello, World!"。

Java 语言为 String 提供了特殊的语法支持,可以直接用双引号 来创建字符串字面量,这在其他语言中是不常见的。
// 使用双引号创建字符串,这是最常见的方式
String str1 = "Hello";
String str2 = "World";
// 使用 new 关键字创建,不推荐(见下文)
String str3 = new String("Hello");
String 的不可变性
这是 String 最核心、最重要的特性。一旦一个 String 对象被创建,它的内容就不能被修改。
什么是不可变性?
- 不能修改内容:你不能像修改数组那样,改变
String对象中某个位置的字符。 - 任何修改操作都是创建新对象:当你对一个
String进行拼接、替换、截取等操作时,JVM 不会在原对象上进行修改,而是会创建一个新的String对象来存放结果,原对象保持不变。
为什么 String 要设计成不可变?
- 线程安全:由于
String对象不可变,它在多个线程之间共享时是绝对安全的,不需要额外的同步机制来保证其一致性,这对于高并发应用至关重要。 - 哈希缓存:
String对象的hashCode()在被创建时就会被计算并缓存,因为内容不会变,所以哈希值也永远不会变,这使得String非常适合用做HashMap、HashSet等哈希表的键,性能更高。 - 字符串常量池:
String的不可变性是实现字符串常量池的基础,由于内容不可变,所以多个字符串变量可以安全地引用同一个字符串字面量,从而节省内存。 - 安全性:在许多场景下,字符串被用于传递敏感信息(如密码、URL),如果字符串是可变的,那么它的内容可能在被传递的过程中被恶意修改,导致安全问题,不可变性保证了字符串内容的安全。
示例:验证不可变性
String s = "hello";
System.out.println("原始字符串: " + s); // 输出: hello
System.out.println("原始对象的内存地址: " + System.identityHashCode(s));
// concat() 方法看起来像是修改了字符串,但实际上它返回了一个新对象
s = s.concat(" world");
System.out.println("修改后的字符串: " + s); // 输出: hello world
System.out.println("新对象的内存地址: " + System.identityHashCode(s));
输出结果:
原始字符串: hello
原始对象的内存地址: 1163157884
修改后的字符串: hello world
新对象的内存地址: 1956725890
可以看到,两次打印的内存地址不同,证明 s 引用了一个全新的对象,而原来的 "hello" 对象并没有被改变。

String 的创建方式
Java 中创建 String 主要有两种方式,它们在内存中的行为有显著区别。
字符串字面量
String str1 = "hello"; String str2 = "hello";
- 过程:当代码执行到
"hello"时,JVM 会首先在字符串常量池 中查找是否存在值为"hello"的字符串。 - 如果存在:直接将引用指向池中的对象。
- 如果不存在:在常量池中创建
"hello"对象,然后将引用指向它。 str1和str2指向的是同一个对象,可以用 来验证( 比较的是内存地址)。
System.out.println(str1 == str2); // 输出: true
new 关键字
String str3 = new String("hello");
String str4 = new String("hello");
- 过程:
- JVM 会在字符串常量池中检查
"hello"是否存在,如果不存在,则先在常量池中创建一个。 new关键字会在堆内存 中创建一个新的String对象,并将常量池中的"hello"内容复制过来。- 将引用
str3指向堆中新创建的对象。
- JVM 会在字符串常量池中检查
str3和str4指向的是堆中两个不同的对象,尽管它们的内容相同,它们都间接引用了常量池中的"hello"。
System.out.println(str3 == str4); // 输出: false System.out.println(str3.equals(str4)); // 输出: true (equals() 比较的是内容)
使用字面量创建 String 可以复用常量池中的对象,更节省内存,除非有特殊需求(如需要一个独立的对象),否则强烈推荐使用字面量。
String 的常用方法
String 类提供了非常丰富的方法来操作字符串。
| 方法类别 | 方法名 | 描述 | 示例 |
|---|---|---|---|
| 获取信息 | length() |
返回字符串长度 | "abc".length() -> 3 |
charAt(int index) |
返回指定索引处的字符 | "abc".charAt(1) -> 'b' |
|
substring(int beginIndex) |
截取从 beginIndex 开始到结尾的子串 |
"abcdef".substring(2) -> "cdef" |
|
substring(int beginIndex, int endIndex) |
截取从 beginIndex 到 endIndex-1 的子串 |
"abcdef".substring(2, 4) -> "cd" |
|
indexOf(String str) |
查找子串首次出现的索引,找不到返回-1 | "hello world".indexOf("world") -> 6 |
|
lastIndexOf(String str) |
查找子串最后一次出现的索引 | "hello world".lastIndexOf("o") -> 7 |
|
| 转换 | toLowerCase() |
转换为小写 | "Hello".toLowerCase() -> "hello" |
toUpperCase() |
转换为大写 | "Hello".toUpperCase() -> "HELLO" |
|
trim() |
去除首尾空白字符 | " hello ".trim() -> "hello" |
|
split(String regex) |
按正则表达式分割字符串,返回字符串数组 | "a,b,c".split(",") -> ["a", "b", "c"] |
|
replace(char old, char new) / replace(CharSequence old, CharSequence new) |
替换所有匹配的字符或子串 | "hello".replace('l', 'p') -> "heppo" |
|
valueOf(...) |
静态方法,将各种数据类型转换为字符串 | String.valueOf(123) -> "123" |
|
| 比较 | equals(Object obj) |
是否相等 | "hello".equals("Hello") -> false |
equalsIgnoreCase(String another) |
忽略大小写比较内容 | "hello".equalsIgnoreCase("Hello") -> true |
|
compareTo(String another) |
按字典序比较大小,返回差值 | "a".compareTo("b") -> -1 |
|
contains(CharSequence s) |
判断是否包含指定子串 | "hello".contains("ell") -> true |
|
startsWith(String prefix) / endsWith(String suffix) |
判断是否以指定前缀/后缀开头/结尾 | "hello".startsWith("he") -> true |
|
| 其他 | format(String format, Object... args) |
格式化字符串 | String.format("我叫%s,今年%d岁", "张三", 18) -> "我叫张三,今年18岁" |
String、StringBuilder 和 StringBuffer 的区别
当需要进行大量字符串拼接操作时,由于 String 的不可变性,频繁创建新对象会带来严重的性能问题,这时就需要使用 StringBuilder 和 StringBuffer。

| 特性 | String |
StringBuilder |
StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 线程安全 | 非线程安全 | 线程安全 |
| 性能 | 差(频繁修改时) | 高 | 较低(因为有同步开销) |
| 使用场景 | 少量字符串定义,或作为不可变的键 | 单线程环境下的字符串拼接 | 多线程环境下的字符串拼接 |
何时使用?
-
使用
String:- 不需要改变。
- 作为
Map的键。 - 少量的字符串操作。
-
使用
StringBuilder:- 在单线程环境中进行大量字符串拼接、修改、删除等操作,这是最常用的场景,性能最佳。
-
使用
StringBuffer:在多线程环境中进行字符串操作,需要保证线程安全,在 Servlet 环境中构建响应。
示例:性能对比
// String 性能差
String s = "";
for (int i = 0; i < 10000; i++) {
s += "a"; // 每次循环都创建新对象
}
// StringBuilder 性能好
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("a"); // 在原对象上操作,不创建新对象
}
String result = sb.toString();
String 的内存布局与 intern() 方法
字符串常量池
字符串常量池是 JVM 的一块特殊内存区域,用于存储字符串字面量,它的主要目的是减少内存占用和提高性能。
intern() 方法
intern() 方法是一个有趣且强大的方法,它可以将字符串放入常量池。
- 对于字面量:
s.intern()会返回常量池中该字符串的引用,如果常量池中没有,则先将字符串内容复制到常量池,再返回引用。 - 对于
new创建的字符串:s.intern()会检查常量池中是否存在与s内容相同的字符串。- 如果存在,则返回常量池中那个字符串的引用。
- 如果不存在,则将该
s的内容复制到常量池,并返回常量池中这个新字符串的引用。
示例:深入理解 intern()
// 情况一:字面量
String s1 = "hello";
String s2 = s1.intern(); // s1 已经在常量池,s2 == s1
System.out.println(s1 == s2); // true
// 情况二:new 创建的字符串
String s3 = new String("hello"); // s3 在堆中
String s4 = s3.intern(); // s3.intern() 会返回常量池中 "hello" 的引用
System.out.println(s3 == s4); // false,因为 s3 在堆,s4 指向常量池
// 情况三:常量池中没有的情况
String s5 = new String("world"); // "world" 不在常量池
String s6 = s5.intern(); // s5.intern() 会将 "world" 放入常量池,并返回其引用
String s7 = "world"; // 字面量,直接指向常量池中的 "world"
System.out.println(s5 == s6); // false
System.out.println(s6 == s7); // true
intern() 方法在处理大量重复且较长的字符串时,可以有效节省内存,但在现代 JVM 中,其性能影响需要谨慎评估。
最佳实践
- 优先使用字面量:除非有特殊需求,否则始终使用
String str = "hello";来创建字符串。 - 避免不必要的字符串拼接:在循环中,不要使用 或 来拼接字符串,这会产生大量临时对象,应使用
StringBuilder。 - 使用
StringBuilder处理大量修改:在需要频繁修改字符串的场景(如循环拼接、格式化),StringBuilder是不二之选。 equals()比较内容, 比较地址:永远记住, 用于判断两个引用是否指向同一个对象,而equals()用于判断两个对象的内容是否相同,对于String时一定要用equals()。- 当心
null:调用null的任何方法都会抛出NullPointerException,在不确定字符串是否为null时,使用Objects.equals(str1, str2)或进行null检查。
希望这份详细的讲解能帮助你全面掌握 Java String 字符串!
