目录
3.3.4. delete() 与 setLength() 方法比较
5.2. AbstractStringBuilder 抽象父类
5.4.1. StringBuffer 的 append() 方法
5.4.2. AbstractStringBuilder 的 append() 方法
5.4.3. ensureCapacityInternal() 方法
一. 前言
在 Java 中,我们除了可以通过 String 类创建和处理字符串之外,还可以使用 StringBuffer 和StringBuilder 类来处理字符串。其中,String 类定义的字符串内容不可变,所以,String 属于不可变字符串。而 StringBuffer 和 StringBuilder 定义的字符串内容可变,这两者属于可变字符串,并且StringBuffer 和 StringBuilder,对字符串的处理效率比 String 类更高。
二. 使用场景
有的小伙伴可能还是不太理解,字符串的使用并不是很难,咱们直接使用 String 来操作就可以了,为什么还要搞出来 StringBuffer 和 StringBuilder 这两个类?这不是找麻烦吗?其实这都是有原因的!
从底层原理来分析,String 构建的字符串对象,其内容理论上是不能被改变的。一旦定义了String 对象就无法再改变其内容,但很多时候我们还是需要改变字符串的内容的,所以 String 类就存在一定的短板。
从应用层面来分析,String 字符串的执行效率其实是比较低的。举个例子,就比如常见的字符串拼接,很多人喜欢使用“+”号来拼接 String 字符串。其实如果是操作少量的字符串,使用 String 还凑活,一旦同时操作的字符串过多,String 的效率就极低了。之前曾做过一个关于10万个字符串拼接的实验。同等条件下,利用“+”号进行拼接所需要的时间是29382毫秒,利用StringBuffer 所需要的时间只有4毫秒,而 StringBuilder 所用的时间更是只需2毫秒,这效率真是天差地别。
另外我们还可以通过下面这个稍微简单点的案例,来看看 Java 底层是如何处理字符串拼接的:
import java.io.PrintStream;
public class StringTest {
public StringTest() {
}
public static void main(String[] args) {
String str = "Hello" + "World";
System.out.println("str=" + str);
}
}
相信很多朋友都会用“+”号来进行字符串拼接,因为觉得该方式简单方便,毕竟一“+”了事。那么利用“+”号来拼接字符串是最好的方案吗?肯定不是的。如果我们使用 JAD 反编译工具对上述 Java字节码进行反编译,你会发现不一样的结果,上述案例反编译后得到的 JAD 文件内容如下所示:
import java.io.PrintStream;
public class StringTest {
public StringTest() {
}
public static void main(String[] args) {
String s = "HelloWorld";
System.out.println((new StringBuilder()).append("str=").append(s).toString());
}
}
从反编译出来的 JAD 文件中我们可以看出,Java 在编译的时候会把“+”号操作符替换成StringBuilder 的 append() 方法。也就是说,“+”号操作符在拼接字符串的时候只是一种形式,让开发者使用起来比较简便,代码看起来比较简洁,但底层使用的还是 StringBuilder 操作。
既然“+”号的底层还是利用 StringBuilder 的 append() 方法操作,那么我们为什么不直接使用StringBuilder 呢?你说对吧,而且当我们需要操作大量的字符串时,更不推荐使用 String,比如:
String str = "";
for (int i = 0; i < 10000; i++) {
str = str + "," + i;
}
上面这段代码,虽然可以实现字符串的拼接,但是在该循环中,每次循环都会创建一个新的字符串对象,然后扔掉旧的字符串。如果是10000次循环,就会执行10000次这样的操作。而这些操作中的绝大部分字符串对象都是临时对象,最终都会被扔掉不用,这就会严重地浪费内存,并会严重影响 GC 垃圾回收的效率。
为了能提高拼接字符串的效率,Java 给我们提供了 StringBuffer 和 StringBuilder,它们都是可变对象,可以预分配缓冲区。当我们往 StringBuffer 或 StringBuilder 中新增字符时,不会创建新的临时对象,可以极大地节省了内存。可以说,好处多多。
三. StringBuffer
3.1. 简介
StringBuffer 是一种可变的字符串类,即在创建 StringBuffer 对象后,我们还可以随意修改字符串的内容。每个 StringBuffer 的类对象都能够存储指定容量的字符串,如果字符串的长度超过了StringBuffer 对象的容量空间,则该对象的容量会自动扩大。
另外我们在使用 StringBuffer 类时,比如每次调用 toString() 方法,都会直接使用缓存区的toStringCache 值来构造一个字符串,这每次都是对 StringBuffer 对象本身进行操作,而不会重新生成一个新对象。所以如果我们需要对大量字符串的内容进行修改,推荐大家使用 StringBuffer。
3.2. 基本特性
StringBuffer 作为一个可变字符串类,具有如下特性:
- 具有线程安全性:StringBuffer 中的公开方法都由 synchronized 关键字修饰,保证了线程同步;
- 带有缓冲区:StringBuffer 每次调用 toString() 方法时,都会直接使用缓存区的 toStringCache 值来构造一个字符串;
- 内容可变性:StringBuffer 中带有字符串缓冲区,我们可以通过数组的复制来实现内容的修改;
- 自带扩容机制:StringBuffer 可以初始化容量,也可以指定容量,当字符串长度超过了指定的容量后,可以通过扩容机制实现长度的变更;
- 内容类型多样性:StringBuffer 中可以存储多种不同类型的数据。
3.3. 基本用法
3.3.1. 常用 API 方法
StringBuffer 作为一个字符串操作类,它有以下几个需要我们掌握的常用 API 方法,如下所示:
| 方法名称 | 方法作用 |
|---|---|
| StringBuffer() | 构造一个空的字符串缓冲区,并且初始化为 16个字符的容量 |
| StringBuffer(int length) | 创建一个空的字符串缓冲区,并且初始化为指定长度 length 的容量 |
| StringBuffer(String str) | 创建一个字符串缓冲区,并将其内容初始化为指定的字符串内容 str,字符串缓冲区的初始容量为 16 加上字符串 str 的长度 |
| StringBuffer append(String s) | 将指定的字符串追加到此字符序列后面 |
| StringBuffer reverse() | 将该字符序进行反转 |
| StringBuffer delete(int start, int end) | 移除该字符串中指定起始位置的子字符串 |
| StringBuffer insert(int offset, int i) | 将int类型的内容插入到该字符串的指定位置上 |
| StringBuffer insert(int offset, String str) | 将String类型的内容插入到字符串的指定位置上 |
| StringBuffer replace(int start, int end, String str) | 使用给定的新子串,替换字符串中指定起始位置上旧的子串 |
| int capacity() | 返回当前字符串的容量 |
| char charAt(int index) | 返回字符串中指定索引处的char值。 |
| int indexOf(String str) | 返回在该字符串中第一次出现指定子串的索引值 |
| int indexOf(String str, int fromIndex) | 从指定索引处开始,返回在该字符串中第一次出现指定子串的索引值 |
| int lastIndexOf(String str) | 返回指定子串在此字符串中最后的索引值 |
| int length() | 返回字符串的长度,即字符个数 |
| CharSequence subSequence(int start, int end) | 根据指定的起、止值,返回一个新的子串 |
| String substring(int start) | 根据指定的起始值,返回一个新的子串 |
3.3.2. 基本案例
知道了这些常用的 API 方法后,我们再通过一个案例来看看这些方法到底是怎么用的。
public class Demo01 {
public static void main(String[] args) {
// 创建StringBuffer对象
StringBuffer sb = new StringBuffer("跟流华追梦1,");
// 在字符串后面追加新的字符串
sb.append("学Java");
System.out.println(sb);
// 删除指定位置上的字符串,从指定的下标开始和结束,下标从0开始
sb.delete(5, 6);
System.out.println(sb); // "跟流华追梦,学Java"
// 在指定下标位置上添加指定的字符串
sb.insert(5, "2");
System.out.println(sb); // 跟流华追梦2,学Java
// 将字符串翻转
sb.reverse();
System.out.println(sb); // avaJ学,2梦追华流跟
// 将StringBuffer转换成String类型
String s = sb.toString();
System.out.println(s);
}
}
运行结果:
跟流华追梦1,学Java
跟流华追梦,学Java
跟流华追梦2,学Java
avaJ学,2梦追华流跟
avaJ学,2梦追华流跟
3.3.3. append() 方法
除了以上几个方法,再重点给大家说一下 append() 追加方法。该方法的作用是追加内容到当前StringBuffer 对象的末尾,类似于字符串的连接。调用该方法以后,StringBuffer 对象的内容也会发生改变。使用该方法进行字符串的连接,会比 String 更加节约内存。我们可以利用 append() 方法进行动态内容的追加,比如进行数据库 SQL 语句的拼接:
public class Demo02 {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
String user = "yyg";
String pwd = "123";
//实现SQL语句的拼接
sb.append("SELECT * FROM t_user WHERE username='")
.append(user)
.append("' AND password='")
.append(pwd)
.append("'");
System.out.println("sql="+sb.toString());
}
}
3.3.4. delete() 与 setLength() 方法比较
StringBuffer 中有 delete()、setLength() 两个方法可以快速清空字符数组。哪个效率高呢?从清空字符串角度看,两者效率都很高,比较来看,setLength效率更高。
这两个函数都是继承自 AbstractStringBuilder 类。函数源码如下:
public AbstractStringBuilder delete(int start, int end) {
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (end > count)
end = count;
if (start > end)
throw new StringIndexOutOfBoundsException();
int len = end - start;
if (len > 0) {
System.arraycopy(value, start+len, value, start, count-end);
count -= len;
}
return this;
}
public void setLength(int newLength) {
if (newLength < 0)
throw new StringIndexOutOfBoundsException(newLength);
ensureCapacityInternal(newLength);
if (count < newLength) {
Arrays.fill(value, count, newLength, '\0');
}
count = newLength;
}
- delete(int start, int end) 删除了 start、end 之间的字符,并返回新的字符串。
- setLength(int newLength) 重新设置了字符长度,如果 newLength 比原长度大,则新增的空间赋值为‘\0’。
两者用途不同,却都可以用于清空字符串。delete(0,AbstractStringBuilder.length) 与setLength(0) 可以达到相同的效果。
比较其源代码之后,发现 setLength() 在清空代码时只是对长度做了一次赋值,delete() 除了对长度赋值外,还做了一次代码 copy 操作。多执行的代码如下:
System.arraycopy(value, start+len, value, start, count-end);
因为 count = end,所以这个 copy 应该也不会执行。但是 delete() 函数多做了一些判断,效率上会比 setLength() 低一点。不过 delete() 多出的条件判断对性能的影响是微乎其微的,通过代码测试没有明显的差异,两者在清空字符串时效率都非常高。
另外,这个两个函数虽然可以将字符串清空,但并没有将资源回收,也就是说并没有达到回收资源的效果,因为 AbstractStringBuilder 的字符数组仍在内存中,只不过我们人为将数组的有效长度置位了 0,数组所占的资源并没有及时释放,只能等 Java 的垃圾回收进行释放。
四. StringBuilder
4.1. 简介
要想实现可变字符串的操作,其实还有另一个 StringBuilder 类,该类是在 Java 5 中被提出的。它和 StringBuffer 的基本用法几乎是完全一样的,关于 StringBuilder 的用法,不会讲解太多。
StringBuilder 和 StringBuffer 最大的不同在于,StringBuilder 的各个方法都不是线程安全的(不能同步访问),在多线程时可能存在线程安全问题,所以 StringBuilder 的执行效率比 StringBuffer快得多。
实际上大多数情况下,我们都是在单线程下进行字符串的操作,使用 StringBuilder 并不会产生线程安全问题。所以针对大多数的单线程情况,还是建议大家使用 StringBuilder,而不是StringBuffer,除非你们的项目对线程安全有着明确的高要求。
4.2. 基本特性
StringBuilder 作为可变字符串操作类,具有如下特性:
- StringBuilder 是线程不安全的,但执行效率更快;
- 适用于单线程环境下,在字符缓冲区进行大量操作的情况。
4.3. 基本用法
StringBuilder 的 API 方法和基本用法与 StringBuffer 一样,此处略过,请参考 StringBuffer 的基本用法。
五. 扩容机制(核心)
在常规的用法上面,StringBuffer 和 StringBuilder 基本没有什么差别。两者的主要区别在于StringBuffer 是线程安全的,但效率低,StringBuilder 是线程不安全的,但效率高。不过在扩容机制上,StringBuffer 和 StringBuilder 是一样的。所以在这里,就以 StringBuffer 为例,只给大家分析一个类即可。
5.1. 继承关系
首先我们可以追踪一下 StringBuffer 的源码,看看它继承自哪个父类:

从上图可以看出,StringBuffer 和 StringBuilder 其实都是继承自 AbstractStringBuilder,所以StringBuffer 与 StringBuilder 这两者可以说是“亲兄弟”的关系,它们俩有一个共同的抽象父类AbstractStringBuilder,如下所示:

5.2. AbstractStringBuilder 抽象父类
我们知道,抽象类可以将多个子类个性化的实现,通过抽象方法交由子类来实现;而多个子类共性的方法,可以放在父类中实现。StringBuffer 和 StringBuilder的共同父类 AbstractStringBuilder 就是一个抽象类,在这个父类中把 StringBuffer 和 StringBuilder 的一些共同内容进行了定义。比如在该类中,就定义了一个定长的字节数组来保存字符串,后面当我们利用 append() 方法不断地追加字符串时,如果该字符串的长度超过了这个数组的长度,就会利用数组复制的方式给该数组进行扩容。

5.3. 容量设置
在前面给大家讲解 StringBuffer 的 API 方法时,也给大家说过 StringBuffer 有3个构造方法。而无论是哪个构造方法都可以设置存储容量,即使是默认的构造方法也会有值为16的存储容量,如下图所示:

5.4. 扩容过程
5.4.1. StringBuffer 的 append() 方法
虽然 StringBuffer 有默认的容量设置,也有自定义的容量设置,但在实际开发过程中,容量还是有可能不够用。这时就会根据追加的字符串长度进行动态扩容,那么这个扩容过程到底是怎么样的呢?其实 StringBuffer 的扩容需要利用 append() 方法作为入口,我们先来看看 append() 方法的源码,如下所示:

5.4.2. AbstractStringBuilder 的 append() 方法
在 StringBuffer 的 append() 方法中,你会发现实际上真正的实现是通过 super 关键字,再调用父类的 append() 方法,所以我们继续往下追踪,此时进入到 AbstractStringBuilder 类中的append() 方法中,如下图所示:

此时我们看到了一个 ensureCapacityInternal() 方法,从字面意思来理解,该方法是用于确保内部容量。传递给该方法的个参数是 count+len,也就是原有字符串的长度+新追加的字符串长度,即 append() 后字符串的总长度。
5.4.3. ensureCapacityInternal() 方法
那么 ensureCapacityInternal() 接受了新字符串的总长度之后会发生什么变化呢?我们必须进入到 ensureCapacityInternal() 方法的内部来探究一番,源码如下:

在该方法中,我们首先看到了一个二进制位的右移运算。value.length 是字符数组的长度,结合coder 参数进行右移运算,得到字符串的原有容量。这里的 coder 参数是一种编码方式,如果字符串中没有中文,默认是采用 Latin1 编码,如果有中文则会采用 UTF-16 编码。因为 UTF-16 编码中文时需要两个字节,也就是说,只要字符串中含有中文,value 字节数组中是每两位对应一个字符。
然后会判断新追加的字符串长度是否超过了 value 字节数组的长度,如果新字符串的长度大于value 字节数组的长度,则说明需要给该字节数组进行扩容。接着就会利用 Arrays.copyOf() 方法,将当前数组的值拷贝给 newCapacity() 个长度的新数组,最后再重新赋值给 value 字节数组。在扩容的过程中,主要是利用数组复制的方法来实现。
5.4.4. newCapacity() 方法
其实讲到现在,关于 StringBuffer 的扩容,基本原理已经给大家讲清楚了,但我们还可以继续深入看看 newCapacity() 这个方法的实现过程与返回值,它与数组扩容密切相关。

该方法的大致作用就是,获取 value 数组的原有长度和待追加的新字符串长度,利用ArraysSupport.newLength() 方法计算出扩容后新数组的长度 length,并最终返回该 length。如果length 的值等于 Integer 的最大值,说明我们传递过来的字符串太长了,就会直接触发一个内存溢出的异常。
5.4.5. newLength() 方法
ArraysSupport.newLength() 方法的内部实现,主要是利用 Math.max() 方法实现的,如下所示:

六. 总结
String、StringBuffer、StringBuilder 这三个类之间的区别主要体现在运行速度、线程安全、功能、可变性这4个方面:
- 在运行速度方面:三者之间的执行速度由快到慢为:StringBuilder > StringBuffer > String;
- 在线程安全方面:StringBuilder 是线程不安全的,而StringBuffer 是线程安全的。如果一个StringBuffer 对象在字符串缓冲区被多个线程使用,StringBuffer 中很多方法都带有synchronized 关键字,可以保证线程是安全的。但 StringBuilder 的方法中则没有该关键字,所以不能保证线程安全,有可能在进行线程并发操作时产生一些异常。所以如果要进行多线程环境下的操作,考虑使用 StringBuffer;在单线程环境下,建议使用 StringBuilder;
- 在功能方面:String 实现了三个接口,即 Serializable、Comparable<String>、CarSequence。StringBuilder 和 StringBuffer 实现了两个接口,Serializable、CharSequence,相比之下 String 的实例可以通过 compareTo 方法进行比较,其他两个不可以。
- 在可变性方面:String 字符串是不可变的,StringBuilder 与 StringBuffer 是可变的。
使用场景:当修改字符串的操作比较多时,可以使用 StringBuilder 或 StringBuffer;在要求线程安全的情况下用 StringBuffer,在不要求线程安全的情况下用 StringBuilder。
- String:适用于少量字符串操作的情况;
- StringBuilder:适用于单线程环境下,在字符缓冲区进行大量操作的情况;
- StringBuffer:适用多线程环境下,在字符缓冲区进行大量操作的情况。
本文详细介绍了Java中StringBuffer和StringBuilder的使用场景、基本特性和核心的扩容机制,对比了它们在运行速度、线程安全性和功能上的异同,以及在不同场景下的选择建议。
7832

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



