String 、StringBuilder 与 StringBuffer 之间的区别
共 9177字,需浏览 19分钟
·
2021-04-22 08:24
点击上方“程序员大白”,选择“星标”公众号
重磅干货,第一时间送达
String
初始化字符串:
String str = "so easy!";
这是字符串初始化的 “简介版本”,通常字符串的初始化有三种方式:
// 1.最常见,简短
String str1 = "Hello wrold!";
// 2.构造函数初始化
String str2 = new String("Hello World!");
// 3.字符数组初始化
char[] charArray = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
String str3 = new String(charArray);
然后我们一起看一下 String 类的源码,再做分析。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/**
* Initializes a newly created {@code String} object so that it represents
* an empty character sequence. Note that use of this constructor is
* unnecessary since Strings are immutable.
*/
public String() {
this.value = new char[0];
}
/**
* Allocates a new {@code String} so that it represents the sequence of
* characters currently contained in the character array argument. The
* contents of the character array are copied; subsequent modification of
* the character array does not affect the newly created string.
*
* @param value
* The initial value of the string
*/
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
...
}
private final char value[];
,可以观察字符串本身是保存在 char 数组中的,换句话说,String 对象其实就是一个字符数组。
这里需要注意的一件非常重要的事情是,String 类被 final
关键字所修饰,public final class String{}
, 这意味着 String 是不可变(immutable)类型。
不可变(immutable) 意味着什么呢?
String str1 = "Hello World!";
str1.substring(1,4).concat("cbl").toLowerCase().trim().replace('b', 'a');
System.out.println(str1); // Hello World!
上面这段程序的输出依旧是 Hello World!
。
因为字符串是 final
的,所以对 str1
所进行的一系列操作并不会改变 str1
本身。这一系列操作返回的是更改后的新字符串,并不会改变 str1
本身。
我们可以看一下 concat()
的源码:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
原始字符串永远不会更改。而是复制一份,并将要连接的文本被添加到复制的字符串之后,最后返回一个新的字符串。
我们用一个变量接收一下返回值,然后输出,发现返回了一个新的字符串 local
:
String str1 = "Hello World!";
String str2 = str1.substring(3,5).concat("cbl").toLowerCase().trim().replace('b', 'a');
System.out.println(str2); // local
JVM 的原生内存模型为:
方便我们对 String 、StringBuffer 和 StringBuilder 的理解,我们仅关心下面图示中的内存区域:
其中 Stack
表示栈区,Heap
表示堆区,String Pool
表示字符串池。
我们再来看看这两个字符串:
String str1 = "algorithm";
String str2 = "algorithm";
实例化的字符串 str1
和 str2
的值保存在Java 堆内存中,堆内存用于为所有 Java 对象动态分配内存。虽然 str1
和 str2
是两个不同的引用变量,但它们都指向 Java 堆内存中的同一块内存位置。
虽然看起来有两个不同的 String 对象,但实际上只有一个。str2
从未被实例化为对象,而是引用了在堆内存中分配给 str1
的对象。
这是由于 Java 针对字符串进行优化的方式造成的。每次实例化字符串对象时,都会将添加到堆内存的值与之前已添加的值进行比较。如果堆内存中已存在相等的值,则不会初始化该对象,而是将该值赋给引用变量。
像 str1
这样的常量值保存在一个称为常量字符串的池中,简称 String Pool,其中包含所有的常量字符串。不过,使用关键字 new
创建的字符串 “偷偷” 绕过了 String Pool,而是直接存储在 Java Heap 当中。
再看一个简单的例子:
String str1 = "algorithm";
String str2 = "algorithm";
String str3 = new String("algorithm");
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str1.equals(str2)); // true
System.out.println(str1.equals(str3)); // false
因为 str1
和 str2
指向内存中的同一对象, str1 == str2
返回为 true
,str1.equals(str2)
也返回 true
。
因为 str3
使用关键字 new
显式实例化,所以 String Pool 中虽然已存在字符串文本,依旧会使用构造函数创建一个新对象。
equals()
方法比较的两个变量的值,而不是它们所指向的内存地址,这就是 str1.equals(str3)
和 str1.equals(str2)
都返回 true
的原因。而 ==
判断的是两个变量所指向的内存地址,比较的是真正意义上的指针操作,所以 str1 == str2
返回 true
,而 str1 == str3
返回 false
。
PS:毫不夸张地说,我的一个在阿里实习过的大佬同学,在面试某国企时就跪在了 equals 和 == 的区别上
需要注意 substring()
和 concat()
方法返回一个新的 String 对象,并将其保存在字符串池中。
我们举的例子都比较简单,但如果我们考虑一些使用数百个字符串变量和数千个操作(如 substring() 或concat()
)的大型项目,仅使用 String 类可能会导致严重的内存泄漏和时间延迟。
这正是为什么还要设计 StringBuffer 或 StringBuilder 类的原因。
StringBuffer & StringBuilder
可变性(mutablity)
StringBuffer
和 StringBuilder
对象与 String
对象一样,三者都是字符序列。但是 StringBuffer
和StringBuilder
是可变的(mutable),String
是不可变的(immutable),这意味着一旦我们为 StringBuffer
或 StringBuilder
初始化一个值,该值就会作为 StringBuffer
或 StringBuilder
对象的属性进行处理。
无论我们修改 StringBuffer
或 StringBuilder
的值多少次,都不会创建新的 String、StringBuffer 或StringBuilder 对象。StringBuffer
或 StringBuilder
的时间效率更高,资源消耗更少。
StringBuilder vs StringBuffer
这两个类几乎完全相同,它们使用同名的方法,返回相同的结果。但它们之间有两个主要区别:
线程安全
StringBuffer
类是同步的(Synchronized),一次只能有一个线程调用StringBuffer
实例的方法。StringBuilder
类是不同步的,多个线程可以调用StringBuilder
类中的方法,而不会被阻塞。因此,StringBuffer
是线程安全的,而StringBuffer
不是。如果我们开发的是多线程的应用程序,那么使用StringBuilder
可能会有风险。效率
StringBuffer
实际上比StringBuilder
慢 2~3 倍。原因是StringBuffer
线程安全(一次只允许一个线程在一个对象上执行),会导致代码执行速度慢得多。
方法
StringBuilder
和 StringBuffer
拥有相同的方法(除 StringBuffer
的方法都是 synchronized
的)。两者常用的几个方法:append()
、insert
、replace()
、delete()
、reverse()
。
String vs StringBuilder vs StringBuffer
String | StringBuilder | StringBuffer | |
---|---|---|---|
可变性 | 不可变 | 可变 | 可变 |
线程安全 | 是 | 否 | 是 |
时间效率 | 低 | 较低 | 高 |
内存效率 | 低 | 高 | 高 |
注意:从上表中看到的,String
类在时间和内存上的效率都较低,但这并不意味着我们不应该再使用它。
事实上,String
可以非常方便地使用,因为它可以快速编写,我在实际开发经常使用 public static final String
来定义字符串常量。
String concatString = "concatString";
StringBuffer appendBuffer = new StringBuffer("appendBuffer");
StringBuilder appendBuilder = new StringBuilder("appendBuilder");
long timerStarted;
timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
concatString += " another string";
}
System.out.println("50000次String concat 操作的时间:" + (System.currentTimeMillis() - timerStarted) + "ms");
timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
appendBuffer.append(" another string");
}
System.out.println("50000次 StringBuffer 的 append 操作时间:" + (System.currentTimeMillis() - timerStarted) + "ms");
timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
appendBuilder.append(" another string");
}
System.out.println("50000次 StringBuilder 的 append 操作时间:" + (System.currentTimeMillis() - timerStarted) + "ms");
输出:
50000次String concat 操作的时间:18108ms
50000次 StringBuffer 的 append 操作时间:7ms
50000次 StringBuilder 的 append 操作时间:3ms
根据您的Java虚拟机的不同,此输出可能会有所不同。仅从这个测试结果可以看出,StringBuilder
在字符串操作方面是最快的。速度次快的是 StringBuffer
,它比 StringBuilder
慢 2~3 倍。String
的 concat
操作最慢。
连接同样多的字符串,StringBuilder
比 String
快约 6000 倍,换句话说,StringBuilder
在 1 秒内可以连接的字符串,String
需要 1.6 小时。
总结
可变性:String 是不可变类型,如果试图更改 String 对象的值,则会在 String Pool 中创建另一个对象,而 StringBuffer 和StringBuilder 是可变的,其值可以更改。
线程安全:StringBuffer 和 StringBuilder 之间的区别就在于 StringBuffer 是线程安全的。因此,当应用程序只需要在单线程中运行时,最好使用 StringBuilder ,因为 StringBuilder 比 StringBuffer 更高效。
使用建议:
如果程序中的字符串不会更改,比如 Java 中常量字符串的定义( public static final String
),建议使用 String 类,因为 String 对象本身就是final
的。如果程序中字符串需要改变,且仅是单线程访问,那么使用 StringBuilder 就足够了。 如果程序中字符串需要改变,且为多个线程访问,建议使用 StringBuffer,因为 StringBuffer 是同步的,可以保证线程安全。
推荐阅读
关于程序员大白
程序员大白是一群哈工大,东北大学,西湖大学和上海交通大学的硕士博士运营维护的号,大家乐于分享高质量文章,喜欢总结知识,欢迎关注[程序员大白],大家一起学习进步!