热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

String,StringBuilder和StringBuffer整理汇总

本文为joshua317原创文章,转载请注明:转载自joshua317博客 https:www.joshua317.comarticle241一、简单了解下,String,Stri

本文为joshua317原创文章,转载请注明:转载自joshua317博客 https://www.joshua317.com/article/241


一、简单了解下,String,StringBuilder和StringBuffer的区别在哪?

String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。String是只读字符串,它并不是基本数据类型,而是一个对象。从底层源码来看是一个final类型的字符数组,所引用的字符串不能被改变,一经定义,无法再增删改。也由于它的不可变性,类似拼接、裁剪字符串等动作,每次对String的操作都会生成新的 String对象

private final char value[];

StringBuffer是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。

StringBuilder是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,减小了开销,是绝大部分情况下进行字符串拼接的首选。

StringBuffer和StringBuilder他们两都继承了AbstractStringBuilder抽象类,从AbstractStringBuilder 抽象类中我们可以看到

/**
* The value is used for character storage.
*/
char[] value;

StringBuilder和StringBuffer的内部实现跟String类一样,都是通过一个char数组存储字符串的,不同的是String类里面的char数组是final修饰的,是不可变的,而StringBuilder和StringBuffer的char数组是可变的。所以在进行频繁的字符串操作时,建议使用StringBuffer和 StringBuilder来进行操作。 另外StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

 



 

 

当我们需要对字符串进行大量修改时,推荐使用StringBufferStringBuilder类。

String类不同,StringBufferStringBuilder类的对象可以反复修改,而不会留下大量新的未使用对象。

StringBuilder类是从Java 5开始引入的,StringBufferStringBuilder之间的主要区别在于StringBuilders方法不是线程安全的(不同步)。

建议尽可能使用StringBuilder类,因为它比StringBuffer更快。 但是,如果需要线程安全性,最好是使用StringBuffer类。


二、代码编程说明StringBuilder不是线程安全的

接下来,我们用StringBuilder和StringBuffer进行字符串追加操作,看下有何不同

我们做下简单的测试

首先,创建10个线程;
然后,每个线程循环100次往StringBuilder或者StringBuffer对象里面append字符。
我们预想结果是输出结果为1000,但是实际运行会输出什么呢?

2.1 StringBuilder测试

package com.joshua317;
public class StringBuilderTest {
public static void main(String[] args) throws InterruptedException {
StringBuilder stringBuilder = new StringBuilder();
for (int i=0; i<10; i++) {
(new Thread(new ThreadTestStringBuilder(stringBuilder))).start();
}
Thread.sleep(100);
System.out.println(stringBuilder.length());
}
}
class ThreadTestStringBuilder implements Runnable {
public StringBuilder stringBuilder;
ThreadTestStringBuilder(StringBuilder stringBuilder) {
this.stringBuilder = stringBuilder;
}
@Override
public void run()
{
for (int j=0; j <100; j++) {
stringBuilder.append("a");
}
}
}

 



 

 

 



 

 


2.2 StringBuffer测试

package com.joshua317;
import java.util.Collection;
public class StringBufferTest {
public static void main(String[] args) throws InterruptedException {
StringBuffer stringBuffer = new StringBuffer();
for (int i=0; i<10; i++) {
(new Thread(new ThreadTestStringBuffer(stringBuffer))).start();
}
Thread.sleep(100);
System.out.println(stringBuffer.length());
}
}
class ThreadTestStringBuffer implements Runnable {
public StringBuffer stringBuffer;
ThreadTestStringBuffer(StringBuffer stringBuffer) {
this.stringBuffer = stringBuffer;
}
@Override
public void run()
{
for (int j=0; j<1000; j++) {
stringBuffer.append("a");
}
}
}

 



 

 

通过上面俩个例子,我们发现,StringBuilder在多线程执行的过程中,可能会出现字符长度小于1000,甚至出现了ArrayIndexOutOfBoundsException异常(异常非必现)。而StringBuffer则正常输出字符串长度为1000,从而我们可以简单得出StringBuilder是非线程安全的


三、分析StringBuilder执行所出现的问题


3.1 为什么输出值跟预期值不一样

我们先看一下StringBuilder的两个成员变量(这两个成员变量实际上是定义在AbstractStringBuilder里面的,StringBuilder和StringBuffer都继承了AbstractStringBuilder)

/**
* The value is used for character storage.
* 用于字符存储
*/
char[] value;
/**
* The count is the number of characters used.
* 用于记录字符数的数量
*/
int count;

 



 

 

再实例化StringBuilder时,我们可以看出,StringBuilder默认的容量大小为16。当然也可以指定初始容量,或者以一个已有的字符序列给StringBuilder对象赋初始值。

StringBuilder stringBuilder = new StringBuilder();
//StringBuilder类
public StringBuilder() {
super(16);
}
//AbstractStringBuilder类
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}

再看StringBuilder的append方法

@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}

 



 

 

然后,StringBuilder的append方法调用了父类AbstractStringBuilder的append方法。

public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}

 



 

 

从代码count += len;我们可以得知它不是一个原子操作。假设这个时候count值为100,len值为1,两个线程同时执行到了这一行,拿到的count值都是101,执行完加法运算后将结果赋值给count,所以两个线程执行完后count值为101,而不是102。这样就导致了字符的计数值要比我们预期结果1000小的原因。

而我们再来看下StringBuffer的append操作

@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}

 



 

 

StringBuffer 的append方法是synchronized 修饰的,保证了多线程情况下,同步操作。


3.2 为什么会抛出ArrayIndexOutOfBoundsException异常

我们看下AbstractStringBuilder的append()方法里面的ensureCapacityInternal()方法,它主要是用来检查StringBuilder对象的原char数组的容量能不能容纳下新的字符串,如果容纳不下就调用ensureCapacityInternal()方法对char数组进行扩容,也就是说StringBuilder 里面的ensureCapacityInternal()方法是用来扩展value数组的。

public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}


private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}


private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length <<1) + 2;
if (newCapacity - minCapacity <0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity <0)
? hugeCapacity(minCapacity)
: newCapacity;
}

private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity <0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}

//java.lang.Integer
@Native public static final int MAX_VALUE = 0x7fffffff;

//java.util.Arrays类中
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

//java.lang.System类中
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);

扩容的方法最终是由expandCapacity()实现的,在这个方法中首先把把容量变为原来容量的2倍加2,如果此时仍小于指定的容量,那么就把新的容量设为minimumCapacity。然后判断是否溢出,如果溢出了,把容量设为MAX_ARRAY_SIZE。最后通过Arrays.copyOf()方法调用System.arraycopy()方法,把value值进行拷贝。

然后继续往下看代码,AbstractStringBuilder类的append()方法中str.getChars(0, len, value, count);

这一行作用是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组里面

str.getChars(0, len, value, count);

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin <0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

我们用流程图来表示字符串拷贝的过程

1.假设现在有两个线程同时执行了StringBuilder的append()方法,两个线程都执行完了AbstractStringBuilder的ensureCapacityInternal()方法,此刻count=101。

 



 

 

2.此时线程1的cpu时间片用完了,线程2继续执行,线程2执行完整个append()方法后count变成102了。



 

 

3.线程1继续执行AbstractStringBuilder的str.getChars()方法的时候拿到的count值就是102了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。

至此,StringBuilder执行所出现的问题已经分析完了,同时StringBuilder多线程不安全的问题也就迎刃而解。


四、扩展


4.1 什么是线程安全?

当多个线程访问某一个类(对象或方法)时,对象对应的公共数据区始终都能表现正确,那么这个类(对象或方法)就是线程安全的。通俗地理解就是:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。


4.2 如何使用String、StringBuffer、StringBuilder呢?























StringStringBufferStringBuilder
不可变字符串可变的字符序列可变的字符序列
 效率低效率高
 线程安全线程不安全

所以,



  • 如果要操作少量的数据使用String;

  • 多线程操作字符串缓冲区下操作大量数据使用StringBuffer;

  • 单线程操作字符串缓冲区下操作大量数据使用StringBuilder。

记住,StringBuffer 适用于用在多线程操作同一个 StringBuffer 的场景,如果是单线程场合 StringBuilder 更适合。


4.3 具体的应用场景



  • 在字符串内容不经常发生变化的业务场景优先使用String类。例如:常量声明、少量的字符串拼接操作等。



  • 在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程环境下,建议使用StringBuffer,例如XML解析、HTTP参数解析与封装。



  • 在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程环境下,建议使用StringBuilder,例如SQL语句拼装、JSON封装等。




4.4 为什么说StringBuilder是线程不安全的?

因为相对StringBuffer,StringBuilder没有在方法上使用 synchronized 关键字。Synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。至于如何分析,请参考上文。

 

本文为joshua317原创文章,转载请注明:转载自joshua317博客 https://www.joshua317.com/article/241



推荐阅读
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • Week04面向对象设计与继承学习总结及作业要求
    本文总结了Week04面向对象设计与继承的重要知识点,包括对象、类、封装性、静态属性、静态方法、重载、继承和多态等。同时,还介绍了私有构造函数在类外部无法被调用、static不能访问非静态属性以及该类实例可以共享类里的static属性等内容。此外,还提到了作业要求,包括讲述一个在网上商城购物或在班级博客进行学习的故事,并使用Markdown的加粗标记和语句块标记标注关键名词和动词。最后,还提到了参考资料中关于UML类图如何绘制的范例。 ... [详细]
author-avatar
原来我不帅S_420
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有