全国旗舰校区

不同学习城市 同样授课品质

北京

深圳

上海

广州

郑州

大连

武汉

成都

西安

杭州

青岛

重庆

长沙

哈尔滨

南京

太原

沈阳

合肥

贵阳

济南

下一个校区
就在你家门口
+
当前位置:首页  >  技术干货

Java StringBuilder与StringBuffer

发布时间:2023-06-13 17:42:00
发布人:zyh

  全文大约【6000】字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

Java StringBuilder与StringBuffer

  一. 可变字符串

  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方法,如下所示:

1683285751534.image

1683285782840.image

  3.2 基本案例

  知道了这些常用的API方法后,我们再通过一个案例来看看这些方法到底是怎么用的。  

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语句的拼接:  

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的源码,看看它继承自哪个父类。

1683286359177.image

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

1683286367098.image

  2.AbstractStringBuilder抽象父类

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

1683286377770.image

  3.容量设置

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

1683286398374.image

  4.扩容过程(核心)

  4.1 StringBuffer#append()方法

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

1683286408404.image

  4.2 AbstractStringBuilder#append()方法

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

1683286415150.image

  此时我们看到了一个ensureCapacityInternal()方法,从字面意思来理解,该方法是用于确保内部容量。传递给该方法的个参数是count+len,也就是 原有字符串的长度+新追加的字符串长度,即append后字符串的总长度。

  4.3 ensureCapacityInternal()方法

  那么ensureCapacityInternal()接受了新字符串的总长度之后会发生什么变化呢?我们必须进入到ensureCapacityInternal()方法的内部来探究一番,源码如下:

1683286423503.image

  在该方法中,我们首先看到了一个二进制位的右移运算。value.length是字符数组的长度,结合coder参数进行右移运算,得到字符串的原有容量。这里的coder参数是一种编码方式,如果字符串中没有中文,默认是采用Latin1编码,如果有中文则会采用UTF-16编码。因为UTF-16编码中文时需要两个字节,也就是说,只要字符串中含有中文,value字节数组中是每两位对应一个字符。

  然后会判断新追加的字符串长度是否超过了value字节数组的长度,如果新字符串的长度大于value字节数组的长度,则说明需要给该字节数组进行扩容。接着就会利用用Arrays.copyOf()方法,将当前数组的值拷贝给newCapacity()个长度的新数组,最后再重新赋值给value字节数组。在扩容的过程中,主要是利用数组复制的方法来实现!

  4.4 newCapacity()方法

  其实讲到现在,关于StringBuffer的扩容,基本原理小编已经给大家讲清楚了,但我们还可以继续深入看看newCapacity()这个方法的实现过程与返回值,它与数组扩容密切相关。

1683286436047.image

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

  4.5 newLength()方法

  而ArraysSupport.newLength()方法的内部实现,主要是利用Math.max()方法实现的,如下所示:

1683286477372.image

  4.6 小结(重点)

  至此,小编就把StringBuffer的扩容过程给大家分析完毕了,最后,小编再给大家把这个扩容的核心思路总结一下,StringBuffer扩容机制的基本规则如下:

  ● 如果一次追加的字符长度超过了当前设置的容量,则会按照 当前容量2+2 进行扩容;

  ● 如果一次追加的长度不仅超过了初始容量,而且按照 当前容量2+2 扩容一次还不够,其容量会直接扩容到与所添加字符串长度相等的长度;

  ● 之后如果还要再追加新的字符内容,依然会按照 当前容量*2+2 进行扩容。

  5. 验证案例

  最后为了验证上述结论是否正确,小编再给大家设计如下案例,供大家思考验证。  

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。

#stringbuilder

相关文章

flutter为什么不使用kotlin作为开发语言?

flutter为什么不使用kotlin作为开发语言?

2023-10-15
opencv和yolo是什么样的关系?

opencv和yolo是什么样的关系?

2023-10-15
矩阵的2范数与向量的2范数有什么关系?

矩阵的2范数与向量的2范数有什么关系?

2023-10-15
什么是逆强化学习?

什么是逆强化学习?

2023-10-15

最新文章

常见网络安全面试题:Windows常用的命令有哪些?

常见网络安全面试题:Windows常用的命令有哪些?

2023-10-09
常见网络安全面试题:根据设备告警如何展开排查?

常见网络安全面试题:根据设备告警如何展开排查?

2023-10-09
常见网络安全面试题:mysql加固呢?(数据库加固)

常见网络安全面试题:mysql加固呢?(数据库加固)

2023-10-09
常见网络安全面试题:windows和linux加固?(操作系统加固)

常见网络安全面试题:windows和linux加固?(操作系统加固)

2023-10-09
在线咨询 免费试学 教程领取