从零开始学Java59之StringBuilder与StringBuffer

前言

最近的这几篇文章,我们一直在给大家讲解字符串相关的内容。其实字符串按照可变性,可以分为不可变字符串与可变字符串。

我们前面学习的String就属于不可变字符串,因为理论上一个String字符串一旦定义好,其内容就不可再被改变,这些内容我们已经在前面都学习过了。但实际上,还有另一种可变字符串,包括StringBuilder和StringBuffer两个类

那可变字符串有什么特点?又怎么使用呢?接下来就请大家跟我一起来学习吧。


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

一. 可变字符串

1. 简介

在Java中,我们除了可以通过String类创建和处理字符串之外,还可以使用StringBuffer

StringBuilder类来处理字符串。

其中,String类定义的字符串内容不可变,所以String属于不可变字符串。

StringBufferStringBuilder定义的字符串内容可变,这两者属于可变字符串,并且StringBufferStringBuilder,对字符串的处理效率比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方法后,我们再通过一个案例来看看这些方法到底是怎么用的。

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 基本案例

知道了这些常用的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.4 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的源码,看看它继承自哪个父类。

从上图可以看出,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. 验证案例

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

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。


今日内容就是这些啦,你学会了吗,可以先收藏或者转发给朋友一起学习哦~

猜你喜欢

转载自blog.csdn.net/GUDUzhongliang/article/details/130410116