Java中的字符串——String,StringBuilder,StringBuffer
1. 字符串基础(String类)
Java中的字符串是
String
类的对象;String
类位于java.lang
包中,无需显式导入;字符串是不可变的(immutable),一旦创建就不能修改
创建字符串的方式:
// 方式1:直接赋值(使用字符串常量池)
String s1 = "Hello";// 方式2:使用new关键字(在堆内存中创建新对象)
String s2 = new String("Hello");
字符串存储机制:
字符串常量池(String Pool)
位于方法区(Java 8及之前)或堆内存(Java 8之后),用于存储字符串字面量,相同内容的字符串字面量会共享常量池中的同一个对象
字符串创建的内存分配
String s1 = "Hello"
: 检查常量池,存在则直接引用,不存在则创建并放入常量池;String s2 = new String("Hello")
: 先在常量池查找"Hello"(存在则引用),然后在堆中创建新对象
String s1 = "Hello"; // 常量池中创建"Hello"
String s2 = new String("Hello"); // 堆中创建新对象,但共享常量池的字符数组System.out.println(s1 == s2); // false,因为一个是常量池对象,一个是堆对象
System.out.println(s1.equals(s2)); // true,内容相同
System.out.println(s1 == "Hello"); // true,都指向常量池中的同一个对象
System.out.println(s2 == "Hello"); // false,s2是堆对象
字符串字面量"Hello"的处理:
当JVM遇到字符串字面量"Hello"时,首先会检查字符串常量池中是否已经存在内容相同的字符串;如果常量池中已经存在"Hello"(比如之前有String s1 = "Hello"
这样的语句),则不会在常量池中创建新的对象,而是直接引用常量池中已有的"Hello"对象new String("Hello")的执行:
new
关键字会在堆内存中创建一个新的String对象,这个新创建的String对象的内容(字符数组)会指向常量池中已有的"Hello"的字符数组(Java 6及之前是复制一份,Java 7+是共享字符数组)(注意:这个新创建的String对象本身不会被放入字符串常量池)
String类型的内部实现:
在Java中,String
类内部使用final char[]
(字符数组,Java 9之前)或byte[]
(字节数组,Java 9及之后)来存储字符串的字符数据。
// Java 8及之前的String内部实现(简化版)
public final class String {private final char value[]; // 存储字符的数组private int hash; // 缓存哈希值// ...
}
从Java 9开始,为了节省内存(特别是ASCII字符串占用的空间),String
内部改用byte[]
存储,并新增一个coder
字段标识编码方式:
// Java 9+的String内部实现(简化版)
public final class String {private final byte[] value; // 存储字符的字节数组private final byte coder; // 0=LATIN1(ASCII), 1=UTF16private int hash; // 缓存哈希值// ...
}
coder
:0
:LATIN1
编码(1字节/字符,适用于纯ASCII字符串)1
:UTF16
编码(2字节/字符,适用于包含非ASCII字符的字符串)
2.Java字符串操作方法汇总表(String类)
分类 | 方法 | 说明 | 示例 |
---|---|---|---|
基本信息 |
| 返回字符串长度(字符数) |
|
| 判断字符串是否为空(长度为0) |
| |
字符访问 |
| 返回指定索引处的字符(索引从0开始) |
|
| 返回指定索引处的Unicode代码点 |
| |
字符串比较 |
| 比较字符串内容是否相等(区分大小写) |
|
| 比较字符串内容是否相等(不区分大小写) |
| |
| 按字典顺序比较字符串(返回int值) |
| |
| 不区分大小写的字典顺序比较 |
| |
查找操作 |
| 返回指定字符首次出现的索引(未找到返回-1) |
|
| 返回指定子字符串首次出现的索引 |
| |
| 返回指定字符最后一次出现的索引 |
| |
| 返回指定子字符串最后一次出现的索引 |
| |
| 判断是否包含指定字符序列 |
| |
转换操作 |
| 转换为全小写 |
|
| 转换为全大写 |
| |
| 去除首尾空白字符 |
| |
| 替换所有指定字符 |
| |
| 替换所有指定子字符串 |
| |
| 使用正则表达式替换所有匹配项 |
| |
| 根据正则表达式分割字符串 |
| |
子字符串 |
| 从指定索引开始到末尾的子字符串 |
|
| 截取从beginIndex到endIndex-1的子字符串 |
| |
前缀/后缀判断 |
| 判断是否以指定前缀开头 |
|
| 判断是否以指定后缀结尾 |
| |
数值转换 |
| 将各种类型转换为字符串(静态方法) |
|
格式化 |
| 格式化字符串(类似C的printf) |
|
连接字符串 |
| 用分隔符连接多个字符串 |
|
其他实用方法 |
| 判断字符串是否匹配正则表达式 |
|
| 按行分割字符串为Stream |
| |
| 重复字符串指定次数 |
|
注意:不可变性:所有字符串操作方法均返回新字符串对象,原始字符串不会被修改。
3.StringBuilder和StringBuffer
StringBuilder和StringBuffer都是Java中用于高效处理可变字符串的类,它们解决了String类不可变带来的性能问题。
1. 基本概念
特性 | StringBuilder | StringBuffer |
---|---|---|
可变性 | 可变 | 可变 |
线程安全 | 非线程安全 | 线程安全 |
引入版本 | Java 5 | Java 1.0 |
共同点:
都是可变的字符序列
都提供了比String更高效的字符串操作方法
都继承自AbstractStringBuilder类
主要区别:
StringBuffer是线程安全的,StringBuilder不是
StringBuffer的方法使用synchronized关键字修饰
StringBuilder性能优于StringBuffer
2. 构造方法
两者都提供了多种构造方法:
// 默认初始容量为16个字符
StringBuilder sb1 = new StringBuilder(); // 默认初始容量16
StringBuffer sbf1 = new StringBuffer(); // 默认初始容量16// 指定初始容量
StringBuilder sb2 = new StringBuilder(100);
StringBuffer sbf2 = new StringBuffer(100);// 用指定字符串初始化
StringBuilder sb3 = new StringBuilder("Hello");
StringBuffer sbf3 = new StringBuffer("World");
3. 核心方法对比
3.1 基本操作方法
方法 | StringBuilder | StringBuffer | 说明 |
---|---|---|---|
| ✔️ | ✔️ | 追加字符串 |
| ✔️ | ✔️ | 在指定位置插入字符串 |
| ✔️ | ✔️ | 删除子字符串 |
| ✔️ | ✔️ | 替换子字符串 |
| ✔️ | ✔️ | 反转字符串 |
| ✔️ | ✔️ | 设置指定位置的字符 |
3.2 线程安全相关
方法 | StringBuilder | StringBuffer | 说明 |
---|---|---|---|
所有公共方法 | 无同步 | 有synchronized修饰 | StringBuffer的方法都是线程安全的 |
4. 性能比较
由于StringBuffer的方法都使用了synchronized关键字进行同步,在单线程环境下:
StringBuilder性能更好:因为它不需要处理线程同步的开销
StringBuffer性能稍差:因为每次方法调用都需要获取和释放锁
在多线程环境下:
StringBuffer更安全:可以保证线程安全
StringBuilder不安全:可能导致数据不一致
5. 使用场景建议及示例
使用StringBuilder的场景:
单线程环境下的字符串拼接操作
性能要求较高的字符串处理
方法内部的临时字符串操作
使用StringBuffer的场景:
多线程环境下的字符串操作
需要线程安全的字符串缓冲区
多个线程可能同时修改同一个字符串缓冲区的情况
StringBuilder示例(单线程):
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
sb.insert(5, ",");
String result = sb.toString(); // "Hello, World"
StringBuffer示例(多线程):
StringBuffer sbf = new StringBuffer();// 线程1
new Thread(() -> {for (int i = 0; i < 100; i++) {sbf.append("A");}
}).start();// 线程2
new Thread(() -> {for (int i = 0; i < 100; i++) {sbf.append("B");}
}).start();// 最终sbf的内容是200个字符,包含A和B的混合
6. 内部实现原理
两者都继承自AbstractStringBuilder类,内部使用字符数组(java9之前)或字节数组(java9+)存储数据:
abstract class AbstractStringBuilder implements Appendable, CharSequence {/*** The value is used for character storage.*/char[] value;/*** The count is the number of characters used.*/int count;
abstract class AbstractStringBuilder implements Appendable, CharSequence {/*** The value is used for character storage.*/byte[] value;/*** The id of the encoding used to encode the bytes in {@code value}.*/byte coder;/*** The count is the number of characters used.*/int count;
....
当容量不足时,会自动扩容(通常是当前容量的2倍+2)。
7. 最佳实践
优先使用StringBuilder:在大多数现代应用中,字符串操作通常在单线程环境下进行,应优先使用StringBuilder。
预估初始容量:如果能预估最终字符串的大致长度,应在构造时指定初始容量,避免频繁扩容。
避免在循环中使用String的+操作:
// 不好 - 每次循环都创建新的String对象 String result = ""; for (int i = 0; i < 100; i++) {result += i; }// 好 - 使用StringBuilder StringBuilder sb = new StringBuilder(); for (int i = 0; i < 100; i++) {sb.append(i); } String result = sb.toString();
多线程环境下必须使用StringBuffer:当多个线程可能同时修改同一个字符串缓冲区时,必须使用StringBuffer。