String
String、StringBuffer、StringBuilder 的别?
String
- 不可变:
String
对象一旦创建,其内容就不能改变。每次对String
的操作(如拼接、替换等)都会生成新的String
对象。 - 线程安全:由于不可变性,
String
是线程安全的。 - 适用场景:适用于不需要频繁修改字符串内容的场景。
StringBuffer
- 可变:
StringBuffer
对象的内容可以修改,且提供了很多用于修改字符串内容的方法(如append()
,insert()
,delete()
等)。 - 线程安全:
StringBuffer
的方法使用了同步机制,因此是线程安全的。 - 适用场景:适用于多线程环境中需要频繁修改字符串内容的场景。
StringBuilder
- 可变:
StringBuilder
与StringBuffer
类似,也是可变的。 - 非线程安全:
StringBuilder
的方法没有使用同步机制,因此不是线程安全的。 - 适用场景:适用于单线程环境中需要频繁修改字符串内容的场景,性能通常比
StringBuffer
更高。
public class StringExamples {
public static void main(String[] args) {
// String 示例
String str = "Hello";
// str = str + " World"; // 每次拼接都会生成新的String对象
String concatenatedStr = str.concat(" World"); // 使用concat方法进行拼接
System.out.println("String: " + concatenatedStr);
// StringBuffer 示例
StringBuffer stringBuffer = new StringBuffer("Hello");
stringBuffer.append(" World");
System.out.println("StringBuffer: " + stringBuffer.toString());
// StringBuilder 示例
StringBuilder stringBuilder = new StringBuilder("Hello");
stringBuilder.append(" World");
System.out.println("StringBuilder: " + stringBuilder.toString());
// 线程安全性示例(仅示意,不建议在生产代码中这样测试)
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
// StringBuffer 是线程安全的
stringBuffer.append(i);
// StringBuilder 不是线程安全的,可能会引发异常或数据不一致
stringBuilder.append(i);
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("StringBuffer after threading: " + stringBuffer.toString());
System.out.println("StringBuilder after threading: " + stringBuilder.toString());
}
}
线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
注意事项
- 性能:对于大量字符串操作,
StringBuilder
通常比String
和StringBuffer
性能更好,因为它没有同步开销。但在多线程环境中,StringBuffer
的线程安全性可能是必要的。 - 不可变性:
String
的不可变性使得它在很多情况下更安全,例如作为HashMap的键时,不会因为内容变化而导致哈希值变化。
String为什么是不可变的?
String
在 Java 中被设计为不可变(immutable)的,这主要是基于以下几个原因:
安全性:
- 不可变性提供了线程安全性。由于
String
对象是不可变的,它们可以被多个线程安全地共享而不需要额外的同步机制。 - 字符串常量池(String Pool)的实现也依赖于字符串的不可变性。字符串常量池是 Java 用来存储字符串字面量的内存区域,它允许相同的字符串字面量只被存储一次,从而节省内存。如果
String
是可变的,那么这种优化就不可能实现,因为相同的字符串引用可能会指向不同的内容。
哈希码缓存:
String
类被用作 Java 集合框架(如HashMap
和HashSet
)中的键。这些集合依赖于对象的哈希码来快速查找元素。由于String
是不可变的,它的哈希码可以在创建时被计算并缓存起来,这样在后续操作中就可以避免重复计算哈希码,从而提高性能。
设计简洁性:
- 不可变性简化了
String
类的设计。如果String
是可变的,那么它的许多方法(如substring()
、replace()
等)都需要返回一个新的String
对象,以保持原始对象的不变性。然而,由于String
本身就是不可变的,这些方法可以直接在原始对象上操作(实际上是通过创建一个新的String
对象来实现的,但对外表现为不可变性),而不需要额外的逻辑来处理可变性。
常量池和字符串字面量:
- Java 编译器和运行时环境对字符串字面量进行了优化,将它们存储在字符串常量池中。这种优化只有在字符串是不可变的情况下才是有效的。如果字符串是可变的,那么编译器就无法确定一个字面量在程序运行期间是否会被修改,因此也就无法安全地将其存储在常量池中。
易于理解和使用:
- 不可变性使得
String
对象的行为更加直观和可预测。当你将一个String
对象传递给一个方法时,你可以确信该方法不会改变你的字符串对象的内容。这有助于减少程序中的错误和调试难度。
字符串拼接用“+”还是 StringBuilder?
Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;
上面的代码对应的字节码如下:
可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。
不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder
以复用,会导致创建过多的 StringBuilder
对象。
String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {
s += arr[i];
}
System.out.println(s);
StringBuilder
对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder
对象。
如果直接使用 StringBuilder
对象进行字符串拼接的话,就不会存在这个问题了。
String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr) {
s.append(value);
}
System.out.println(s);
如果你使用 IDEA 的话,IDEA 自带的代码检查机制也会提示你修改代码。
在 JDK 9 中,字符串相加“+”改为用动态方法 makeConcatWithConstants()
来实现,通过提前分配空间从而减少了部分临时对象的创建。然而这种优化主要针对简单的字符串拼接,如: a+b+c
。对于循环中的大量拼接操作,仍然会逐个动态分配内存(类似于两个两个 append 的概念),并不如手动使用 StringBuilder 来进行拼接效率高。