Java中StringBuilder与StringBuffer使用及源码解读
作者:一一哥Sun 时间:2022-05-16 09:11:18
一. 可变字符串
1. 简介
在Java中,我们除了可以通过String类创建和处理字符串之外,还可以使用StringBuffer和StringBuilder类来处理字符串。其中,String类定义的字符串内容不可变,所以String属于不可变字符串。而StringBuffer和StringBuilder定义的字符串内容可变,这两者属于可变字符串,并且StringBuffer和StringBuilder,对字符串的处理效率比String类更高。
2. 使用场景
有的小伙伴可能还是不太理解,字符串的使用并不是很难,咱们直接使用String来操作就可以了,为什么还要搞出来StringBuffer和StringBuilder这两个类?这不是找麻烦吗?其实这都是有原因的!
从底层原理来分析,String构建的字符串对象,其内容理论上是不能被改变的。一旦定义了String对象就无法再改变其内容,但很多时候我们还是需要改变字符串的内容的,所以String类就存在一定的短板。
另外从应用层面来分析,String字符串的执行效率其实是比较低的。举个例子,就比如常见的字符串拼接,很多人喜欢使用“+号”来拼接String字符串。其实如果是操作少量的字符串,使用String还凑活,一旦同时操作的字符串过多,String的效率就极低了。之前曾做过一个关于10万个字符串拼接的实验。同等条件下,利用“+”号进行拼接所需要的时间是29382毫秒,利用StringBuffer所需要的时间只有4毫秒,而StringBuilder所用的时间更是只需2毫秒,这效率真是天差地别!
另外我们还可以通过下面这个稍微简单点的案例,来看看Java底层是如何处理字符串拼接的。
String str = "Hello" + "World";
System.out.println("str=" + str);
相信很多朋友都会用 “+”号 来进行字符串拼接,因为觉得该方式简单方便,毕竟 一 “+” 了事。那么利用 “+”号来拼接字符串是最好的方案吗?肯定不是的!如果我们使用JAD反编译工具对上述Java字节码进行反编译,你会发现不一样的结果,上述案例反编译后得到的JAD文件内容如下所示:
import java.io.PrintStream;
public class StringTest13{
public StringTest13(){
}
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、StringBuilder的用法吧。
二. StringBuffer
1. 简介
StringBuffer是一种可变的字符串类,即在创建StringBuffer对象后,我们还可以随意修改字符串的内容。每个StringBuffer的类对象都能够存储指定容量的字符串,如果字符串的长度超过了StringBuffer对象的容量空间,则该对象的容量会自动扩大。
另外我们在使用StringBuffer类时,比如每次调用toString()方法,都会直接使用缓存区的toStringCache 值来构造一个字符串,这每次都是对StringBuffer对象本身进行操作,而不会重新生成一个新对象。所以如果我们需要对大量字符串的内容进行修改,推荐大家使用StringBuffer。
2. 基本特性
StringBuffer作为一个可变字符串类,具有如下特性:
具有线程安全性:StringBuffer中的公开方法都由synchronized关键字修饰,保证了线程同步;
带有缓冲区:StringBuffer每次调用toString()方法时,都会直接使用缓存区的toStringCache值来构造一个字符串;
内容可变性:StringBuffer中带有字符串缓冲区,我们可以通过数组的复制来实现内容的修改;
自带扩容机制:StringBuffer可以初始化容量,也可以指定容量,当字符串长度超过了指定的容量后,可以通过扩容机制实现长度的变更;
内容类型多样性:StringBuffer中可以存储多种不同类型的数据。
了解了StringBuffer的基本特性之后,请大家跟着小编来学习一下StringBuffer的基本用法吧。
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.2 基本案例
知道了这些常用的API方法后,我们再通过一个案例来看看这些方法到底是怎么用的。
/**
* @author
*/
public class Demo01 {
public static void main(String[] args) {
//创建StringBuffer对象
StringBuffer sb = new StringBuffer("跟一一哥,");
//在字符串后面追加新的字符串
sb.append("学Java!");
System.out.println(sb);
//删除指定位置上的字符串,从指定的下标开始和结束,下标从0开始
sb.delete(2, 4);
System.out.println(sb);//"一哥"
//在指定下标位置上添加指定的字符串
sb.insert(2, "123");
System.out.println(sb);//跟一123,学Java!
//将字符串翻转
sb.reverse();
System.out.println(sb);//!avaJ学,321一跟
//将StringBuffer转换成String类型
String s = sb.toString();
System.out.println(s);
}
}
3.3 append()用法
在以上几个方法中,再重点给大家说一下append()追加方法。该方法的作用是追加内容到当前StringBuffer对象的末尾,类似于字符串的连接。调用该方法以后,StringBuffer对象的内容也会发生改变。使用该方法进行字符串的连接,会比String更加节约内存。我们可以利用append()方法进行动态内容的追加,比如进行数据库SQL语句的拼接:
/**
* @author
*/
public class Demo02 {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
String user = "yyg";
String pwd = "123";
//实现SQL语句的拼接
sb.append("select * from userInfo where username=")
.append(user)
.append(" and pwd=")
.append(pwd);
System.out.println("sql="+sb.toString());
}
}
StringBuffer的用法其实很简单,和String差不多,大家简单掌握即可。
三. StringBuilder
1. 简介
要想实现可变字符串的操作,其实还有另一个StringBuilder类,该类是在Java 5中被提出的。它和 StringBuffer的基本用法几乎是完全一样的,关于StringBuilder的用法,不会讲解太多。
但StringBuilder和StringBuffer最大的不同在于,StringBuilder的各个方法都不是线程安全的(不能同步访问),在多线程时可能存在线程安全问题,但StringBuilder的执行效率却比StringBuffer快的多。
实际上大多数情况下,我们都是在单线程下进行字符串的操作,所以使用StringBuilder并不会产生线程安全问题。所以针对大多数的单线程情况,还是建议大家使用StringBuilder,而不是StringBuffer,除非你们的项目对线程安全有着明确的高要求。
2. 特性
StringBuilder作为可变字符串操作类,具有如下特性:
StringBuilder是线程不安全的,但执行效率更快;
适用于单线程环境下,在字符缓冲区进行大量操作的情况。
3. 基本用法
StringBuilder的API方法和基本用法与StringBuffer一样,此处略过。
四. 扩容机制(重点)
扩容机制应该是本篇文章中的一个重难点,所以要结合源码,单独列出一节给大家仔细分析一下。
在常规的用法上面,StringBuffer和StringBuilder基本没有什么差别。两者的主要区别在于StringBuffer是线程安全的,但效率低,StringBuilder是线程不安全的,但效率高。不过在扩容机制上,StringBuffer和StringBuilder是一样的。所以在这里,就以StringBuffer为例,只给大家分析一个类即可。
1. 继承关系
首先我们可以追踪一下StringBuffer的源码,看看它继承自哪个父类。
从上图可以看出,StringBuffer和StringBuilder其实都是继承自AbstractStringBuilder,所以StringBuffer与StringBuilder这两者可以说是“亲兄弟”的关系,它们俩有一个共同的抽象父类AbstractStringBuilder,如下所示:
2. AbstractStringBuilder抽象父类
在之前给大家讲解抽象类时就跟大家说过,抽象类可以将多个子类个性化的实现,通过抽象方法交由子类来实现;而多个子类共性的方法,可以放在父类中实现。StringBuffer和StringBuilder的共同父类AbstractStringBuilder就是一个抽象类,在这个父类中把StringBuffer和StringBuilder的一些共同内容进行了定义。比如在该类中,就定义了一个定长的字节数组来保存字符串,后面当我们利用append()方法不断地追加字符串时,如果该字符串的长度超过了这个数组的长度,就会利用数组复制的方式给该数组进行扩容。
3. 容量设置
另外在前面给大家讲解StringBuffer的API方法时,也给大家说过StringBuffer有3个构造方法。而无论是哪个构造方法都可以设置存储容量,即使是默认的构造方法也会有值为16的存储容量,如下图所示:
4. 扩容过程(核心)
4.1 StringBuffer#append()方法
虽然StringBuffer有默认的容量设置,也有自定义的容量设置,但在实际开发过程中,容量还是有可能不够用。这时就会根据追加的字符串长度进行动态扩容,那么这个扩容过程到底是怎么样的呢?其实StringBuffer的扩容需要利用append()方法作为入口,我们先来看看append()方法的源码,如下所示:
4.2 AbstractStringBuilder#append()方法
在StringBuffer的append()方法中,你会发现实际上真正的实现是通过super关键字,在调用父类的append()方法,所以我们继续往下追踪,此时进入到AbstractStringBuilder类中的append()方法中,如下图所示:
此时我们看到了一个ensureCapacityInternal()方法,从字面意思来理解,该方法是用于确保内部容量。传递给该方法的个参数是count+len,也就是 原有字符串的长度+新追加的字符串长度,即append后字符串的总长度。
4.3 ensureCapacityInternal()方法
那么ensureCapacityInternal()接受了新字符串的总长度之后会发生什么变化呢?我们必须进入到ensureCapacityInternal()方法的内部来探究一番,源码如下:
在该方法中,我们首先看到了一个二进制位的右移运算。value.length是字符数组的长度,结合coder参数进行右移运算,得到字符串的原有容量。这里的coder参数是一种编码方式,如果字符串中没有中文,默认是采用Latin1编码,如果有中文则会采用UTF-16编码。因为UTF-16编码中文时需要两个字节,也就是说,只要字符串中含有中文,value字节数组中是每两位对应一个字符。
然后会判断新追加的字符串长度是否超过了value字节数组的长度,如果新字符串的长度大于value字节数组的长度,则说明需要给该字节数组进行扩容。接着就会利用用Arrays.copyOf()方法,将当前数组的值拷贝给newCapacity()个长度的新数组,最后再重新赋值给value字节数组。在扩容的过程中,主要是利用数组复制的方法来实现!
4.4 newCapacity()方法
其实讲到现在,关于StringBuffer的扩容,基本原理已经给大家讲清楚了,但我们还可以继续深入看看newCapacity()这个方法的实现过程与返回值,它与数组扩容密切相关。
该方法的大致作用就是,获取value数组的原有长度和待追加的新字符串长度,利用ArraysSupport.newLength()方法计算出扩容后新数组的长度length,并最终返回该length。如果length的值等于Integer的最大值,说明我们传递过来的字符串太长了,就会直接触发一个内存溢出的异常。
4.5 newLength()方法
而ArraysSupport.newLength()方法的内部实现,主要是利用Math.max()方法实现的,如下所示:
4.6 小结(重点)
至此,就把StringBuffer的扩容过程给大家分析完毕了,最后,再给大家把这个扩容的核心思路总结一下,StringBuffer扩容机制的基本规则如下:
如果一次追加的字符长度超过了当前设置的容量,则会按照 当前容量*2+2 进行扩容;
如果一次追加的长度不仅超过了初始容量,而且按照 当前容量*2+2 扩容一次还不够, 其容量会直接扩容到与所添加字符串长度相等的长度 ;
之后如果还要再追加新的字符内容,依然会按照 当前容量*2+2 进行扩容。
5. 验证案例
最后为了验证上述结论是否正确,再给大家设计如下案例,供大家思考验证。
/**
* @author
*/
public class Demo03 {
// 扩容机制
public static void main(String[] args) {
//无参构造方法,初始容量默认为16
StringBuffer sb = new StringBuffer();
//使用StringBuffer的capacity()方法查看其当前容量
System.out.println("默认初始化容量capacity=" + sb.capacity() + ",默认长度length=" + sb.length());
//一次追加20个字符,因为超过了初始容量,因此会扩容16*2+2=34
sb.append("11111111112222222222");
System.out.println("扩容一次的capacity()=" + sb.capacity() + ",扩容一次后的length=" + sb.length());
StringBuffer sb02 = new StringBuffer();
//再次添加50个字符,不仅超过了初始容量16,而且按照 当前容量*2+2 进行扩容(34)后,依然存储不下,
//则直接将容量扩容到新追加的字符串长度50
sb02.append("11111111112222222222333333333344444444445555555555");
System.out.println("再次扩容后的capacity="+sb02.capacity()+",再次扩容后的长度length():"+sb02.length());
}
}
从上述实验的执行结果中,你会发现StringBuffer与StringBuilder就是按照上述规则进行扩容的。
五. 结语
至此,我们就把字符串相关的内容都学习完了,接下来就把今天的重点内容给大家总结一下,尤其是String、StringBuffer与StringBuilder的区别有哪些。
1. 相同点
String、StringBuffer、StringBuilder三者共同之处,它们都是final类,不允许被继承,这样设计主要是从性能和安全性上考虑的。
2. 不同点
String、StringBuffer、StringBuilder这三个类之间的区别主要体现在3个方面,即 运行速度、线程安全、功能、可变性 这4个方面。
在运行速度方面: 三者之间的执行速度由快到慢为:StringBuilder > StringBuffer > String
在线程安全方面: StringBuilder是线程不安全的,而StringBuffer是线程安全的。 如果一个StringBuffer对象在字符串缓冲区被多个线程使用,StringBuffer中很多方法都带有synchronized关键字,可以保证线程是安全的。但StringBuilder的方法中则没有该关键字,所以不能保证线程安全,有可能在进行线程并发操作时产生一些异常。所以如果要进行多线程环境下的操作,考虑使用StringBuffer;在单线程环境下,建议使用速度StringBuilder。
在功能方面: String实现了三个接口,即Serializable、Comparable、CarSequence; StringBuilder和StringBuffer实现了两个接口,Serializable、CharSequence,相比之下String的实例可以通过compareTo方法进行比较,其他两个不可以。
在可变性方面:String字符串是不可变的,StringBuilder与StringBuffer是可变的。
3. 最后总结一下
String: 适用于少量字符串操作的情况;
StringBuilder:适用于单线程环境下,在字符缓冲区进行大量操作的情况;
StringBuffer: 适用多线程环境下,在字符缓冲区进行大量操作的情况;
使用场景 : 当修改字符串的操作比较多时,可以使用StringBuilder或StringBuffer;在要求线程安全的情况下用StringBuffer,在不要求线程安全的情况下用StringBuilder。
来源:https://juejin.cn/post/7235628080219488293