概要
在Java编程中,字符串是我们最常打交道的数据类型之一。然而,面对String、StringBuffer和StringBuilder这三个看似相似的类,很多开发者都会感到困惑:它们到底有什么区别?在何种场景下该使用谁?本文将带你拨开迷雾,深入剖析这“字符串三兄弟”的底层原理与核心差异,让你在日后开发中能够做出最明智、最高效的选择。
1. String(字符串)—— 写在石头上的字
通俗理解:
String 就像你用凿子在石头上刻字。一旦刻上去,这个字就永远不能修改了。如果你想在“Hello”后面加一个“World”,你不是在原石头上修改,而是必须找一块全新的石头,重新刻上“Hello World”。原来的那块写着“Hello”的石头还在,只是你不再用它了。
专业解释:
-
不可变性: String 对象一旦被创建,其值就不能被改变。任何对 String 的修改(如拼接、替换)操作,实际上都是创建了一个全新的 String 对象。
-
字符串常量池: 为了提高效率,Java 有一个特殊的内存区域叫“字符串常量池”。当你写
String s1 = "Hello",Java 会先去池里找有没有现成的“Hello”,如果有就直接用,没有才创建一个。这可以节省内存。
举例说明:
String str = "Hello";
str = str + " World"; // 看起来是修改了str,其实是创建了一个新对象"Hello World"
System.out.println(str); // 输出:Hello World
// 我们来验证一下它是否是新对象
String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");
String str4 = str1 + " World";
System.out.println(str1 == str2); // true,因为它们指向常量池里的同一个对象
System.out.println(str1 == str3); // false,str3是强制在堆里新创建的对象
System.out.println(str1.equals(str3)); // true,equals比较的是内容,所以为真
public class StringExample {
public static void main(String[] args) {
String result = "";
// 在循环中拼接字符串 - 非常低效!
for (int i = 0; i < 5; i++) {
result += "数字:" + i + " "; // 每次循环都创建新对象
System.out.println("第" + i + "次循环,result的内存地址:" + System.identityHashCode(result));
}
System.out.println("最终结果:" + result);
}
}
输出效果:
第0次循环,result的内存地址:1324119927
第1次循环,result的内存地址:1878246837
第2次循环,result的内存地址:1430317222
第3次循环,result的内存地址:2065951873
第4次循环,result的内存地址:1791741888
最终结果:数字:0 数字:1 数字:2 数字:3 数字:4
注意: 每次循环result的内存地址都不同,说明每次都创建了新对象!
特点总结:
-
优点: 安全(因为不可变)、简单。
-
缺点: 频繁修改时(比如在循环中拼接),会产生大量垃圾对象,效率非常低。
2. StringBuffer(字符串缓冲区)—— 写在黑板上的字(带锁)
通俗理解:
StringBuffer 就像一块黑板。你可以在上面随意写字、擦掉、添加内容。最关键的是,这块黑板带有一把锁。当一个人(一个线程)在上面写字时,他会把门锁上,其他人只能在外面等着,等他写完了,下一个人才可以进来写。这保证了不会出现两个人同时修改,把字写花的情况。
专业解释:
-
可变性: StringBuffer 是一个可变的字符序列。你可以调用它的方法(如
.append(),.insert(),.delete())来修改内容,而不会创建新对象。 -
线程安全: 它的所有方法都是
synchronized(同步的),意味着它是多线程安全的。多个线程同时操作时,数据不会错乱。
举例说明:
// 创建一个StringBuffer,初始内容是"Hello"
StringBuffer sbuffer = new StringBuffer("Hello");
// 进行各种修改,都是在原对象上操作
sbuffer.append(" World"); // 拼接:Hello World
sbuffer.insert(5, ","); // 在索引5的位置插入逗号:Hello, World
sbuffer.delete(5, 6); // 删除索引5到6之间的字符(删掉了逗号):Hello World
sbuffer.reverse(); // 反转:dlroW olleH
System.out.println(sbuffer.toString()); // 输出:dlroW olleH
特点总结:
-
优点: 可变的,频繁修改时效率高;线程安全。
-
缺点: 因为加了锁,在单线程环境下,性能上有一点点损耗。
3. StringBuilder(字符串建造者)—— 写在笔记本上的字
通俗理解:
StringBuilder 和 StringBuffer 几乎一模一样,也是一块可以随意涂改的“黑板”(可变字符序列)。唯一的区别是,这块黑板没有锁。它就像你自己的私人笔记本,只有你一个人在用,所以不需要上锁,想怎么写就怎么写,速度更快。
专业解释:
-
可变性: 和 StringBuffer 一样,是可变的。
-
非线程安全: 它的方法不是
synchronized的。因此,它在单线程环境下性能比 StringBuffer 更高。但如果多个线程同时操作它,可能会导致数据不一致。
举例说明:
它的用法和 StringBuffer 完全一样,只是名字不同。
// StringBuilder的用法和StringBuffer一模一样
StringBuilder sbuilder = new StringBuilder("Hello");
sbuilder.append(" World");
sbuilder.insert(5, ",");
sbuilder.delete(5, 6);
sbuilder.reverse();
System.out.println(sbuilder.toString()); // 输出:dlroW olleH
public class StringBuilderExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append("数字:").append(i).append(" "); // 在原对象上修改
System.out.println("第" + i + "次循环,sb的内存地址:" + System.identityHashCode(sb));
}
System.out.println("最终结果:" + sb.toString());
}
}
输出效果:
第0次循环,sb的内存地址:1324119927
第1次循环,sb的内存地址:1324119927
第2次循环,sb的内存地址:1324119927
第3次循环,sb的内存地址:1324119927
第4次循环,sb的内存地址:1324119927
最终结果:数字:0 数字:1 数字:2 数字:3 数字:4
注意: 内存地址始终不变,说明始终是同一个对象!
特点总结:
-
优点: 可变的,在单线程下,它是三者中性能最高的。
-
缺点: 线程不安全。
性能测试
public class PerformanceTest {
public static void main(String[] args) {
int count = 10000; // 测试10000次拼接
// String 测试
long startTime1 = System.currentTimeMillis();
String str = "";
for (int i = 0; i < count; i++) {
str += "test";
}
long endTime1 = System.currentTimeMillis();
System.out.println("String 耗时:" + (endTime1 - startTime1) + " 毫秒");
// StringBuilder 测试
long startTime2 = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count; i++) {
sb.append("test");
}
long endTime2 = System.currentTimeMillis();
System.out.println("StringBuilder 耗时:" + (endTime2 - startTime2) + " 毫秒");
// StringBuffer 测试
long startTime3 = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < count; i++) {
sbf.append("test");
}
long endTime3 = System.currentTimeMillis();
System.out.println("StringBuffer 耗时:" + (endTime3 - startTime3) + " 毫秒");
}
}
输出结果:可以看到性能差距非常大!
String 耗时:208 毫秒
StringBuilder 耗时:1 毫秒
StringBuffer 耗时:2 毫秒
小结
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(因为不可变) | 安全(同步锁) | 不安全 |
| 性能 | 修改时最低 | 较高(因有锁,稍慢) | 最高 |
| 使用场景 | 操作少量数据,或不需要改变字符串内容 | 多线程环境下,操作大量可变字符串 | 单线程环境下,操作大量可变字符串 |
| 场景 | 选择 | 理由 |
|---|---|---|
| 操作少,字符串不变 | String | 简单安全,利用常量池 |
| 单线程,频繁操作 | StringBuilder | 性能最优,无锁开销 |
| 多线程,频繁操作 | StringBuffer | 线程安全,防止并发问题 |
String三兄弟核心区别解析
69

被折叠的 条评论
为什么被折叠?



