【浅谈Java】String、StringBuffer与StringBuilder

在Java 编程中会广泛应用到字符串,在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。需要注意的是,String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,这样不仅效率低下,而且大量浪费有限的内存空间。为了应对经常性的字符串相关的操作,谷歌引入了两个新的类——StringBuffer类和StringBuild类来对此种变化字符串进行处理。

String、StringBuffer与StringBuilder

一、String

String 是被 final 修饰的类,不能被继承;String实现了 Serializable 和 Comparable 接口,表示String支持序列化和可以比较大小;String底层是通过char类型的数据实现的,并且被final修饰,所以字符串的值创建之后就不可以被修改,具有不可变性。

String 类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。

例如:

String a = "123";
a = "456";

System.out.println(a)	// 打印出来的a为456 

咦,这不是明明已经对他进行修改了吗?为什么说他是一个不可变类呢?接下来就看一张上述a对象的内存存储空间图

在这里插入图片描述

可以看出,再次给a赋值时,并不是对原来堆中实例对象进行重新赋值,而是生成一个新的实例对象,并且指向“456”这个字符串,a则指向最新生成的实例对象,之前的实例对象仍然存在,如果没有被再次引用,则会被垃圾回收。

1. String的实例化方式

  1. 通过字面量方式实例化
String str = "abc"; 
  1. 通过new+构造器的方式实例化
String str=new String("abc"); 

区别: 通过字面量方式为字符串赋值时,此时的字符串存储在方法区的字符串常量池中;
通过new+构造器方式实例化字符串时,字符串对象存储在堆中,但是字符串的值仍然存储在方法区的常量池中。

String字符串具有不可变性,当字符串重新赋值时,不管是对字符串进行拼接,还是调用String的replace()方法修改指定的字符或字符串,都不会在原来的内存地址进行修改,而是重新分配新的内存地址进行赋值。

二、StringBuffer

StringBuffer对象代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列,但都不会产生新的对象。通过StringBuffer生成的字符串,可以调用toString()方法将其转换为一个String对象。

例如:

StringBuffer b = new StringBuffer("123");
b.append("456");

System.out.println(b);	// b打印结果为:123456 

在看一下b对象的内存空间图:

在这里插入图片描述

可以看到它没有重新生成一个对象,而且在原来的对象中可以连接新的字符串。所以说StringBuffer对象是一个字符序列可变的字符串。

三、StringBuilder

StringBuilder类也代表可变字符串对象。实际上,StringBuilder和StringBuffer基本相似,他们的原理与操作一样,两个类的构造器和方法也基本相同。不同的是:StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。

四、StringBuffer如何实现线程安全

StringBuffer类中实现的方法:
在这里插入图片描述

StringBuilder类中实现的方法:

在这里插入图片描述

可以看到,StringBuffer类中的方法都添加了synchronized关键字,也就是给这个方法添加了一个锁,用来保证线程安全。

五、StringBuffer和StringBuilder的区别

img

  1. 线程安全

    StringBuffer 线程安全,StringBuilder 线程不安全。因为 StringBuffer 的所有公开方法都是 synchronized修饰的,而 StringBuilder 并没有synchronized修饰。

  2. 缓冲区

    StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。StringBuffer 的这个toString 方法仍然是同步的。

    而 StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。

  3. 性能

    StringBuffer 是线程安全的,它的所有公开方法都是同步的,StringBuilder 是没有对方法加锁同步的,所以StringBuilder 的性能要大于 StringBuffer。

    StringBuffer 适用于用在多线程操作同一个 StringBuffer 的场景,而 StringBuilder 更适合单线程场合。

六、String、StringBuffer和StringBuilder的异同

相同点: 都用来代表字符串,底层都是通过char数组实现的。
不同点:

  1. String类是不可变类,String对象一旦创建,其值是不能修改的,如果要修改,会重新开辟内存空间来存储修改之后的对象;而StringBuffer和StringBuilder对象的值是可以被修改的;即任何对String的改变会引发新的String对象的生成。

  2. StringBuffer和StringBuilder类则是可变类,他俩的原理和操作基本相同,任何对它所指代的字符串的改变都不会产生新的对象。StringBuffer几乎所有的方法都使用synchronized实现了同步,支持并发操作,线程安全,在多线程系统中可以保证数据同步,但是效率比较低;而StringBuilder线程不安全,不支持并发操作,不能同步访问,不适合多线程中使用。但是效率比较高。

  3. 需要对字符串进行频繁的修改,不要使用String,否则会造成内存空间的浪费。考虑线程安全的场合使用 StringBuffer,如果不需要考虑线程安全,追求效率的场合可以使用 StringBuilder。

七、StringBuffer的扩容机制

StringBuffer和StringBuilder都是继承自AbstractStringBuilder,它们两个的区别在于buffer是线程安全的,builder是线程不安全的,前者安全效率低,后者高效不安全。它们的扩容机制也是这样的区别,所以我们只需要分析一个的扩容就可以了,分析buffer,另一个只用把synchronized关键字去掉就是一样的。

1. 初始容量

既然是容器,那么是一定会有个初始容量的,目的在于避免在内存中过度占用内存。容器的初始容量有默认和使用构造函数申明两种。

StringBuffer类可以创建可修改的字符串序列,该类有以下三个改造方法。

  1. StringBuffer()的初始容量可以容纳16个字符,当该对象的实体存放的字符的长度大于16时,实体容量就自动增加。StringBuffer对象可以通过length()方法获取实体中存放的字符序列长度,通过 capacity()方法来获取当前实体的实际容量。

  2. StringBuffer(int size)可以指定分配给该对象的实体的初始容量参数为参数size指定的字符个数。当该对象的实体存放的字符序列的长度大于size个字符时,实体的容量就自动的增加。以便存放所增加的字符。

  3. StringBuffer(String s)可以指定给对象的实体的初始容量为参数字符串s的长度额外再加16个字符。当该对象的实体存放的字符序列长度大于size个字符时,实体的容量自动的增加,以便存放所增加的字符。

2. 如何扩容

首先我们需要知道StringBuffer和StringBuilder类都继承了抽象类 AbstractStringBuilder类。

查看父类 AbstractStringBuilder的构造函数,发现底层是一个字符数组来保存字符串的。

AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    } 

再来看看StringBuffer的构造函数

  • 无参构造函数,默认容量为16
public StringBuffer() {
        super(16);
    } 
  • 带参(参数为字符串)构造函数,默认容量为参数字符串长度加上16
public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    } 
  • 带参(参数为int容量)构造函数,容量大小为指定的大小
public StringBuffer(int capacity) {
        super(capacity);
    } 

源码都调用父类来进行初始化。

3. 扩容原理

算法原理:

使用append()方法在字符串后面追加值的时候,如果长度超过了该字符串存储空间大小了就就会先进性扩容。构建新的并且存储空间更大的字符串,将旧的复制过去。

在进行字符串append添加的时候,会先计算添加后字符串大小,传入一个方法:ensureCapacityInternal 这个方法进行是否扩容的判断,需要扩容就调用expandCapacity方法进行扩容。

扩容规则:

  • 先 原始容量 * 2 + 2(加2是因为拼接字符串通常末尾都会有个多余的字符)
  • 如果扩容了之后,容量够用,新的容量就为扩容之后的容量。
  • 如果扩容了之后,容量不够用,新的容量就是所需要的容量,即原始字符串长度加上新添加的字符串长度。
  • 扩容完成之后,将原始数组复制到新的容量中,然后将新的字符串添加进去

感谢大家的耐心阅读,如有建议请私信或评论留言

posted @ 2022-11-16 18:05  杨业壮  阅读(102)  评论(0编辑  收藏  举报