作者:孙玉昌,昵称【一一哥】,另外【壹壹哥】也是我哦
千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者
最近的这几篇文章,壹哥一直在给大家讲解字符串相关的内容。其实字符串按照可变性,可以分为不可变字符串与可变字符串。我们前面学习的String就属于不可变字符串,因为理论上一个String字符串一旦定义好,其内容就不可再被改变,这些内容我们已经在前面都学习过了。但实际上,还有另一种可变字符串,包括StringBuilder和StringBuffer两个类。那可变字符串有什么特点?又怎么使用呢?接下来就请大家跟我一起来学习吧。
-----------------------------------------------前戏已做完,精彩即开始---------------------------------------------
全文大约【6000】字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......
Github:
GitHub - SunLtd/LearnJava
Gitee:
从零开始学Java: 从零开始学Java系列稀土掘金专栏地址:https://juejin.cn/column/7175082165548351546CSDN专栏地址:https://yiyige.blog.csdn.net/article/details/129377219?spm=1001.2014.3001.5502
在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毫秒,这效率真是天差地别!关于本实验,请大家参考如下链接:
高薪程序员&面试题精讲系列16之Java中如何拼接多个字符串?StringBuffer和StringBuilder的区别?
另外我们还可以通过下面这个稍微简单点的案例,来看看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是一种可变的字符串类,即在创建StringBuffer对象后,我们还可以随意修改字符串的内容。每个StringBuffer的类对象都能够存储指定容量的字符串,如果字符串的长度超过了StringBuffer对象的容量空间,则该对象的容量会自动扩大。
另外我们在使用StringBuffer类时,比如每次调用toString()方法,都会直接使用缓存区的toStringCache 值来构造一个字符串,这每次都是对StringBuffer对象本身进行操作,而不会重新生成一个新对象。所以如果我们需要对大量字符串的内容进行修改,壹哥推荐大家使用StringBuffer。
StringBuffer作为一个可变字符串类,具有如下特性:
- 具有线程安全性:StringBuffer中的公开方法都由synchronized关键字修饰,保证了线程同步;
- 带有缓冲区:StringBuffer每次调用toString()方法时,都会直接使用缓存区的toStringCache值来构造一个字符串;
- 内容可变性:StringBuffer中带有字符串缓冲区,我们可以通过数组的复制来实现内容的修改;
- 自带扩容机制:StringBuffer可以初始化容量,也可以指定容量,当字符串长度超过了指定的容量后,可以通过扩容机制实现长度的变更;
- 内容类型多样性:StringBuffer中可以存储多种不同类型的数据。
了解了StringBuffer的基本特性之后,请大家跟着壹哥来学习一下StringBuffer的基本用法吧。
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) |
根据指定的起始值,返回一个新的子串 |
知道了这些常用的API方法后,我们再通过一个案例来看看这些方法到底是怎么用的。
/**
* @author 一一哥Sun
*/
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);
}
}
在以上几个方法中,壹哥再重点给大家说一下append()追加方法。该方法的作用是追加内容到当前StringBuffer对象的末尾,类似于字符串的连接。调用该方法以后,StringBuffer对象的内容也会发生改变。使用该方法进行字符串的连接,会比String更加节约内存。我们可以利用append()方法进行动态内容的追加,比如进行数据库SQL语句的拼接:
/**
* @author 一一哥Sun
*/
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差不多,大家简单掌握即可。
与本节内容配套的视频链接如下:
External Player - 哔哩哔哩嵌入式外链播放器
要想实现可变字符串的操作,其实还有另一个StringBuilder类,该类是在Java 5中被提出的。它和 StringBuffer的基本用法几乎是完全一样的,关于StringBuilder的用法,壹哥不会讲解太多。
但StringBuilder和StringBuffer最大的不同在于,StringBuilder的各个方法都不是线程安全的(不能同步访问),在多线程时可能存在线程安全问题,但StringBuilder的执行效率却比StringBuffer快的多。
实际上大多数情况下,我们都是在单线程下进行字符串的操作,所以使用StringBuilder并不会产生线程安全问题。所以针对大多数的单线程情况,壹哥还是建议大家使用StringBuilder,而不是StringBuffer,除非你们的项目对线程安全有着明确的高要求。
StringBuilder作为可变字符串操作类,具有如下特性:
- StringBuilder是线程不安全的,但执行效率更快;
- 适用于单线程环境下,在字符缓冲区进行大量操作的情况。
StringBuilder的API方法和基本用法与StringBuffer一样,此处略过。
与本节内容配套的视频链接如下:
External Player - 哔哩哔哩嵌入式外链播放器
扩容机制应该是本篇文章中的一个重难点,所以壹哥要结合源码,单独列出一节给大家仔细分析一下。
在常规的用法上面,StringBuffer和StringBuilder基本没有什么差别。两者的主要区别在于StringBuffer是线程安全的,但效率低,StringBuilder是线程不安全的,但效率高。不过在扩容机制上,StringBuffer和StringBuilder是一样的。所以在这里,壹哥就以StringBuffer为例,只给大家分析一个类即可。
首先我们可以追踪一下StringBuffer的源码,看看它继承自哪个父类。
从上图可以看出,StringBuffer和StringBuilder其实都是继承自AbstractStringBuilder,所以StringBuffer与StringBuilder这两者可以说是“亲兄弟”的关系,它们俩有一个共同的抽象父类AbstractStringBuilder,如下所示:
壹哥在之前给大家讲解抽象类时就跟大家说过,抽象类可以将多个子类个性化的实现,通过抽象方法交由子类来实现;而多个子类共性的方法,可以放在父类中实现。StringBuffer和StringBuilder的共同父类AbstractStringBuilder就是一个抽象类,在这个父类中把StringBuffer和StringBuilder的一些共同内容进行了定义。比如在该类中,就定义了一个定长的字节数组来保存字符串,后面当我们利用append()方法不断地追加字符串时,如果该字符串的长度超过了这个数组的长度,就会利用数组复制的方式给该数组进行扩容。
另外壹哥在前面给大家讲解StringBuffer的API方法时,也给大家说过StringBuffer有3个构造方法。而无论是哪个构造方法都可以设置存储容量,即使是默认的构造方法也会有值为16的存储容量,如下图所示:
虽然StringBuffer有默认的容量设置,也有自定义的容量设置,但在实际开发过程中,容量还是有可能不够用。这时就会根据追加的字符串长度进行动态扩容,那么这个扩容过程到底是怎么样的呢?其实StringBuffer的扩容需要利用append()方法作为入口,我们先来看看append()方法的源码,如下所示:
在StringBuffer的append()方法中,你会发现实际上真正的实现是通过super关键字,在调用父类的append()方法,所以我们继续往下追踪,此时进入到AbstractStringBuilder类中的append()方法中,如下图所示:
此时我们看到了一个ensureCapacityInternal()方法,从字面意思来理解,该方法是用于确保内部容量。传递给该方法的个参数是count+len,也就是 原有字符串的长度+新追加的字符串长度,即append后字符串的总长度。
那么ensureCapacityInternal()接受了新字符串的总长度之后会发生什么变化呢?我们必须进入到ensureCapacityInternal()方法的内部来探究一番,源码如下:
在该方法中,我们首先看到了一个二进制位的右移运算。value.length是字符数组的长度,结合coder参数进行右移运算,得到字符串的原有容量。这里的coder参数是一种编码方式,如果字符串中没有中文,默认是采用Latin1编码,如果有中文则会采用UTF-16编码。因为UTF-16编码中文时需要两个字节,也就是说,只要字符串中含有中文,value字节数组中是每两位对应一个字符。
然后会判断新追加的字符串长度是否超过了value字节数组的长度,如果新字符串的长度大于value字节数组的长度,则说明需要给该字节数组进行扩容。接着就会利用用Arrays.copyOf()方法,将当前数组的值拷贝给newCapacity()个长度的新数组,最后再重新赋值给value字节数组。在扩容的过程中,主要是利用数组复制的方法来实现!
其实讲到现在,关于StringBuffer的扩容,基本原理壹哥已经给大家讲清楚了,但我们还可以继续深入看看newCapacity()这个方法的实现过程与返回值,它与数组扩容密切相关。
该方法的大致作用就是,获取value数组的原有长度和待追加的新字符串长度,利用ArraysSupport.newLength()方法计算出扩容后新数组的长度length,并最终返回该length。如果length的值等于Integer的最大值,说明我们传递过来的字符串太长了,就会直接触发一个内存溢出的异常。
而ArraysSupport.newLength()方法的内部实现,主要是利用Math.max()方法实现的,如下所示:
至此,壹哥就把StringBuffer的扩容过程给大家分析完毕了,最后,壹哥再给大家把这个扩容的核心思路总结一下,StringBuffer扩容机制的基本规则如下:
- 如果一次追加的字符长度超过了当前设置的容量,则会按照 当前容量*2+2 进行扩容;
- 如果一次追加的长度不仅超过了初始容量,而且按照 当前容量*2+2 扩容一次还不够,其容量会直接扩容到与所添加字符串长度相等的长度;
- 之后如果还要再追加新的字符内容,依然会按照 当前容量*2+2 进行扩容。
最后为了验证上述结论是否正确,壹哥再给大家设计如下案例,供大家思考验证。
/**
* @author 一一哥Sun
* V我领资料:syc_2312119590
* 各平台都有壹哥的同名博客哦
*/
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就是按照上述规则进行扩容的。
与本节内容配套的视频链接如下:
External Player - 哔哩哔哩嵌入式外链播放器
-------------------------------------------------正片已结束,来根事后烟-----------------------------------------------
至此,我们就把字符串相关的内容都学习完了,接下来壹哥就把今天的重点内容给大家总结一下,尤其是String、StringBuffer与StringBuilder的区别有哪些。
String、StringBuffer、StringBuilder三者共同之处,它们都是final类,不允许被继承,这样设计主要是从性能和安全性上考虑的。
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是可变的。
String:适用于少量字符串操作的情况;
StringBuilder:适用于单线程环境下,在字符缓冲区进行大量操作的情况;
StringBuffer:适用多线程环境下,在字符缓冲区进行大量操作的情况;
使用场景:当修改字符串的操作比较多时,可以使用StringBuilder或StringBuffer;在要求线程安全的情况下用StringBuffer,在不要求线程安全的情况下用StringBuilder。
另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。
在给定的字符串“ABCDEFGhijklmn1234567”中,随机获取4个字符,并使用 StringBuilder拼接成字符串。随机获取到的4个字符中可以出现重复字符。
提示:大家可以通过创建随机数对象来实现
java.util.Random。
java.util.Random random = new java.util.Random();
random.nextInt(100); //可以获取到 0~99 中任意一个随机数