目录
String的基本概念String的不可变性String的常用方法String的创建方式String、StringBuilder和StringBuffer的区别String的内存布局与常量池- 最佳实践
String 的基本概念
String 类代表一个不可变的字符序列,在 Java 中,字符串被广泛用于存储和操作文本数据。

- 本质:
String不是基本数据类型,它是一个对象。 - 声明:使用
String关键字来声明字符串变量。String greeting = "Hello, World!";
String 的不可变性
这是 String 最核心、最重要的一个特性,一旦一个 String 对象被创建,它的内容就不能被改变。
如何体现不可变性?
当你对字符串进行任何修改操作(如 concat(), replace(), substring() 等)时,Java 不会修改原始的 String 对象,而是会创建一个新的 String 对象来保存修改后的结果。
示例代码:

String str1 = "hello";
System.out.println("str1 的初始值: " + str1); // 输出: hello
System.out.println("str1 的内存地址: " + System.identityHashCode(str1)); // 获取对象哈希码
// str1.concat(" world") 会返回一个新的字符串对象
// str1 本身并没有改变
str1 = str1.concat(" world");
System.out.println("str1 修改后的值: " + str1); // 输出: hello world
System.out.println("str1 修改后的内存地址: " + System.identityHashCode(str1)); // 地址发生了变化
输出分析:
str1 的初始值: hello
str1 的内存地址: 1163157884 (假设的地址)
str1 修改后的值: hello world
str1 修改后的内存地址: 1956725890 (地址不同,说明是新的对象)
为什么 String 要设计成不可变?
- 线程安全:不可变对象天生就是线程安全的,多个线程可以同时访问一个
String对象,而无需担心数据被其他线程修改,因此不需要额外的同步机制。 - 安全性:字符串被广泛用于 Java 类的加载、网络连接的 URL、文件路径等,如果字符串是可变的,这些关键信息可能会被恶意代码篡改,导致严重的安全问题。
- 性能优化(哈希缓存):因为字符串内容不会改变,
String类的hashCode()方法在第一次被调用后,会缓存计算好的哈希值,之后每次调用hashCode()都直接返回缓存值,这大大提高了在哈希表(如HashMap,HashSet)中作为键的性能。 - 作为
HashMap的键:HashMap要求键对象的hashCode和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 begin, int end) |
返回从 beginIndex 到 endIndex-1 的子字符串。 |
"abcdef".substring(2, 4) -> "cd" |
concat(String str) |
将指定字符串连接到此字符串的末尾。 | "a".concat("b") -> "ab" |
replace(char old, char new) / replace(CharSequence old, CharSequence new) |
替换所有出现的旧字符/序列为新字符/序列。 | "hello".replace('l', 'p') -> "heppo" |
contains(CharSequence s) |
判断字符串是否包含指定的字符序列。 | "hello world".contains("world") -> true |
toUpperCase() / toLowerCase() |
将字符串转换为大写/小写。 | "Hello".toLowerCase() -> "hello" |
trim() |
去除字符串两端的空白字符。 | " hello ".trim() -> "hello" |
split(String regex) |
根据给定的正则表达式分割字符串。 | "a,b,c".split(",") -> ["a", "b", "c"] |
equals(Object anObject) |
比较两个字符串的内容是否相等(区分大小写)。 | "abc".equals("ABC") -> false |
equalsIgnoreCase(String anotherString) |
比较两个字符串的内容是否相等(不区分大小写)。 | "abc".equalsIgnoreCase("ABC") -> true |
compareTo(String anotherString) |
按字典顺序比较两个字符串。 | "abc".compareTo("abd") -> -1 |
indexOf(int ch) / lastIndexOf(int ch) |
返回指定字符第一次/最后一次出现的索引。 | "hello".indexOf('l') -> 2 |
valueOf(primitive type or Object) |
将基本数据类型或对象转换为字符串。 | String.valueOf(123) -> "123" |
String 的创建方式
在 Java 中,创建字符串主要有两种方式,它们在内存中的行为有很大不同。
字面量赋值
String str1 = "hello"; String str2 = "hello";
- 机制:这种方式会首先在 JVM 的字符串常量池 中查找是否存在
"hello"这个字符串,如果存在,则直接引用该对象;如果不存在,则在常量池中创建"hello",然后再引用它。 - 比较:
str1 == str2的结果是true,因为它们引用的是常量池中的同一个对象。System.out.println(str1 == str2); // true
new 关键字创建
String str3 = new String("hello");
String str4 = new String("hello");
- 机制:这种方式会在堆内存中创建一个新的
String对象,其内容是"hello",它不会去检查常量池中是否已经存在"hello"(尽管"hello"这个字面量本身会被放入常量池)。 - 比较:
str3 == str4的结果是false,因为它们是堆中两个不同的对象。System.out.println(str3 == str4); // false
| 特性 | 字面量赋值 (String s = "abc") |
new 关键字 (String s = new String("abc")) |
|---|---|---|
| 创建位置 | 字符串常量池 | 堆内存 |
| 内存检查 | 会检查常量池,有则复用,无则创建 | 直接在堆中创建新对象,不检查常量池(但字面量本身会入池) |
| 比较 | 引用同一个对象时为 true |
永远是 false(除非是同一个变量) |
| 性能 | 更高效,节省内存 | 较低,会创建不必要的额外对象 |
最佳实践:在大多数情况下,优先使用字面量赋值,除非你有明确的理由需要在运行时动态创建字符串。
String、StringBuilder 和 StringBuffer 的区别
由于 String 的不可变性,在进行大量字符串拼接操作时,会频繁创建新对象,导致性能低下。StringBuilder 和 StringBuffer 就是为了解决这个问题而设计的。
| 特性 | String |
StringBuilder |
StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 线程安全(不可变) | 非线程安全 | 线程安全 |
| 性能 | 差(频繁创建对象) | 高 | 稍低(因为有同步开销) |
| 主要用途 | 存储和操作少量、固定的文本 | 在单线程环境下进行大量字符串修改 | 在多线程环境下进行大量字符串修改 |
| 示例方法 | concat(), replace() |
append(), delete(), reverse() |
append(), delete(), reverse() |
如何选择?
- 如果字符串内容不会改变:使用
String。String name = "John"; - 如果需要在单线程下进行大量字符串拼接、修改:使用
StringBuilder,这是最常见的场景,性能最好。StringBuilder sb = new StringBuilder(); sb.append("Hello"); sb.append(" "); sb.append("World"); String result = sb.toString(); // "Hello World" - 如果需要在多线程环境下进行字符串修改:使用
StringBuffer,它的方法大多被synchronized修饰,保证了线程安全,但速度比StringBuilder慢。
String 的内存布局与常量池
理解常量池对于深入理解 String 至关重要。
-
字符串常量池:是 JVM 内存中一个特殊的区域,专门用于存储字符串字面量,它的目的是为了复用字符串,减少内存开销。
-
intern()方法:这是一个不常用但很重要的方法,它的作用是:如果当前字符串在常量池中不存在,则将其添加到常量池中,并返回常量池中的引用;如果已存在,则直接返回常量池中的引用。String s1 = new String("hello"); String s2 = s1.intern(); // s2 现在指向常量池中的 "hello" String s3 = "hello"; System.out.println(s1 == s2); // false, s1在堆,s2在常量池 System.out.println(s2 == s3); // true, s2和s3都指向常量池中的同一个对象
最佳实践
-
优先使用字面量:除非有特殊需求,否则始终用
String s = "hello";的方式创建字符串。 -
避免不必要的拼接:在循环中不要使用 或
concat()来拼接字符串,这会产生大量中间对象,应使用StringBuilder。// 错误示例 String result = ""; for (int i = 0; i < 1000; i++) { result = result + i; // 每次循环都创建新对象 } // 正确示例 StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1000; i++) { sb.append(i); } String result = sb.toString(); -
使用
equals()比较内容:要比较两个字符串的内容是否相同,务必使用equals()或equalsIgnoreCase()方法。 比较的是内存地址(引用),这在大多数情况下不是你想要的。 -
注意
split()的正则表达式:split()方法接收的是一个正则表达式,而不是普通的字符串,如果你要分割的字符串中包含正则表达式中的特殊字符(如 、、 等),需要进行转义。// 错误:. 在正则中匹配任意字符 String[] parts1 = "192.168.1.1".split("."); System.out.println(parts1.length); // 输出可能是 0,而不是 4 // 正确:使用 \\ 进行转义 String[] parts2 = "192.168.1.1".split("\\."); System.out.println(parts2.length); // 输出 4
希望这份详细的讲解能帮助你彻底理解 Java 中的 String!
