Java 字符串详解
String 是 Java 中最常用的类,理解其内部机制对性能优化至关重要。
一、String 特性
1.1 不可变性
String s = "Hello";
s = s + " World"; // 创建了新对象
// "Hello" 对象本身没有被修改
为什么不可变:
- 安全性:字符串常用于参数(数据库 URL、文件路径)
- 线程安全:天然线程安全,可共享
- 缓存哈希:hashCode 可缓存,提升性能
- 字符串池:支持字符串常量池
1.2 源码结构
public final class String implements Serializable, Comparable<String>, CharSequence {
// JDK 8:char 数组
private final char value[];
// JDK 9+:byte 数组 + 编码标记
private final byte[] value;
private final byte coder; // LATIN1 或 UTF16
// 缓存的哈希值
private int hash;
}
二、字符串常量池
2.1 什么是常量池
字符串常量池 = JVM 专门存储字符串的区域
↓
相同字符串只保存一份
↓
节省内存,提高效率
2.2 创建方式对比
// 方式 1:字面量(从常量池)
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true(同一对象)
// 方式 2:new 关键字(堆中创建)
String s3 = new String("Hello");
String s4 = new String("Hello");
System.out.println(s3 == s4); // false(不同对象)
System.out.println(s3.equals(s4)); // true(内容相同)
2.3 intern() 方法
String s1 = new String("Hello");
String s2 = s1.intern();
String s3 = "Hello";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true(都指向常量池)
// intern() 作用:
// 1. 如果常量池已有,返回池中的引用
// 2. 如果常量池没有,添加到池中并返回
2.4 常量池位置
JDK 6 及之前:永久代(PermGen)
JDK 7:移到堆中
JDK 8+:元空间(Metaspace),但字符串池仍在堆中
三、字符串拼接
3.1 拼接方式对比
// 方式 1:+ 操作符
String s1 = "Hello" + " " + "World";
// 编译优化为 StringBuilder
// 方式 2:StringBuilder
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" ").append("World");
String s2 = sb.toString();
// 方式 3:StringBuffer(线程安全)
StringBuffer sbf = new StringBuffer();
sbf.append("Hello").append(" ").append("World");
String s3 = sbf.toString();
// 方式 4:String.join(JDK 8+)
String s4 = String.join(" ", "Hello", "World");
// 方式 5:String.format
String s5 = String.format("%s %s", "Hello", "World");
3.2 性能测试
// 测试代码
int count = 10000;
// + 拼接(循环内)
long start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < count; i++) {
s += i; // 每次创建新对象
}
System.out.println("+ 拼接:" + (System.currentTimeMillis() - start) + "ms");
// 约 5000ms
// StringBuilder
start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append(i);
}
String s2 = sb.toString();
System.out.println("StringBuilder: " + (System.currentTimeMillis() - start) + "ms");
// 约 5ms
// 性能差异 1000 倍!
3.3 编译优化
// 源码
String s = "Hello" + " " + "World";
// 编译后
String s = new StringBuilder("Hello")
.append(" ")
.append("World")
.toString();
// 注意:仅在编译期可确定时优化
// 运行期 + 不会优化
String a = "Hello";
String b = a + " World"; // 不会优化为 StringBuilder
四、String vs StringBuilder vs StringBuffer
4.1 核心区别
| 特性 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 是 | 否 | 是(synchronized) |
| 性能 | 低 | 高 | 中 |
| 适用场景 | 不频繁修改 | 单线程频繁修改 | 多线程频繁修改 |
4.2 源码对比
// StringBuilder(无锁)
public StringBuilder append(String str) {
super.append(str);
return this;
}
// StringBuffer(有锁)
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
4.3 选择建议
// ✅ 使用 String
String name = "Alice"; // 不修改
String sql = "SELECT * FROM users"; // 常量
// ✅ 使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i); // 单线程频繁修改
}
// ✅ 使用 StringBuffer
StringBuffer sbf = new StringBuffer();
// 多线程共享
new Thread(() -> sbf.append("A")).start();
new Thread(() -> sbf.append("B")).start();
五、常用 API
5.1 查找与匹配
String s = "Hello, World!";
// 查找
s.indexOf("World"); // 7
s.lastIndexOf("o"); // 8
s.contains("World"); // true
s.startsWith("Hello"); // true
s.endsWith("!"); // true
// 匹配
s.matches(".*World.*"); // true
"abc123".matches("\\d+"); // false
5.2 截取与分割
String s = "Hello, World!";
// 截取
s.substring(7); // "World!"
s.substring(0, 5); // "Hello"
// 分割
"1,2,3".split(","); // ["1", "2", "3"]
"a.b.c".split("\\.", 2); // ["a", "b.c"]
// 连接
String.join("-", "a", "b", "c"); // "a-b-c"
5.3 转换
// 大小写
"Hello".toLowerCase(); // "hello"
"hello".toUpperCase(); // "HELLO"
// 去除空格
" hello ".trim(); // "hello"
" hello ".strip(); // "hello" (JDK 11+)
// 替换
"Hello".replace('l', 'L'); // "HeLLo"
"Hello".replaceAll("l", "L"); // "HeLLo"
// 编码
"你好".getBytes(StandardCharsets.UTF_8);
new String(bytes, StandardCharsets.UTF_8);
六、性能优化
6.1 避免创建过多对象
// ❌ 创建多个对象
String s = "".trim().toUpperCase().trim();
// ✅ 链式调用
String s = original.trim().toUpperCase().trim();
6.2 使用合适的数据结构
// ❌ 字符串拼接
String result = "";
for (String s : list) {
result += s;
}
// ✅ StringBuilder
StringBuilder sb = new StringBuilder();
for (String s : list) {
sb.append(s);
}
String result = sb.toString();
6.3 预分配容量
// ❌ 默认容量(16),可能多次扩容
StringBuilder sb = new StringBuilder();
// ✅ 预估大小
StringBuilder sb = new StringBuilder(1000);
6.4 使用 String.format 替代 +
// ❌ + 拼接(可读性差)
String msg = "User " + name + " is " + age + " years old";
// ✅ String.format(可读性好)
String msg = String.format("User %s is %d years old", name, age);
七、最佳实践
7.1 字符串比较
// ❌ 使用 ==
if (s == "Hello") {}
// ✅ 使用 equals
if ("Hello".equals(s)) {} // 常量在前,避免 NPE
7.2 空字符串判断
// ❌ 冗长
if (s != null && s.length() > 0) {}
// ✅ 简洁
if (s != null && !s.isEmpty()) {}
// ✅ JDK 11+
if (s != null && !s.isBlank()) {} // 包括空白字符
7.3 敏感信息处理
// ❌ String 存储密码(不可变,无法清除)
String password = "secret";
// ✅ char[] 存储密码(可清除)
char[] password = "secret".toCharArray();
// 使用后立即清除
Arrays.fill(password, ' ');
7.4 大字符串处理
// ❌ 内存溢出
String large = new String(new byte[Integer.MAX_VALUE]);
// ✅ 使用流或分块处理
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) {
// 逐行处理
}
}
八、总结
字符串核心要点:
| 类 | 可变性 | 线程安全 | 适用场景 |
|---|---|---|---|
| String | 不可变 | 是 | 不频繁修改 |
| StringBuilder | 可变 | 否 | 单线程频繁修改 |
| StringBuffer | 可变 | 是 | 多线程频繁修改 |
理解字符串常量池和不可变性,选择合适的字符串类,是性能优化的关键。