Java基础——StringBuilder和StringBuffer



前言

此文主要针对StringBuffer和StringBuilder做一些解读。


一、String

1、String不可变的原理

提到字符串 String,大家第一形象就是不可变,但是当我们直接再次修改字符串时,会变成新建一个字符串,而不是在原先基础上修改
为什么会新建一个对象?

  • 1、可以从两个方面理解:语法方面内存方面
  • 2、语法方面:String 内部存储字符串其实是由一个字符数组存储,其修饰符为 private final char value[];,因为字符数组被 private final 修饰意味着不可更改,所以定义在其内的字符串也不可更改
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];   
    ....
    }
  • 3、内存方面:String定义的字符串,会存储至堆中的字符串常量池中,当字符串常量池中,有该字符串时,可以直接引用,但是没有时,会创建一个字符串存入字符串常量池中
    如下:name存储在栈中,只是一个引用,现在把字符串"晚归的生活"存入常量池中,但是当再次给 name 赋值时,字符串常量池中并没有该字符串"晚归的生活很帅“,所以会创建一个新的字符串存储入常量池中。
    public static void main(String[] args) {
        String name = "晚归的生活";    
        name = "晚归的生活很帅";
        System.out.println(name);
    }

2、final 扩展

final 表示最终的,不可更改的。可能一部分初学者看到String 、StringBuffer、StringBuilder源码会发现,三者都被 final 修饰,为什么String是不可变的,另外两者是可变的呢,这其实是对 final 不够深入了解导致的,所以在这扩展一下。

  • 1、修饰类时:表示该类不可被继承
  • 2、修饰方法时:表示该方法不可被重写
  • 3、修饰变量时:有两种情况,修饰基础数据类型、引用类型,但数据不可改
  • 4、修饰对象时:对象的指向地址不可更改,但值可以更改
    public static void main(String[] args) {
        final int[] arr ={1,2,3};
        arr[1] = 10;   //修改数值
        System.out.println(Arrays.toString(arr));  输出[1, 10, 3]
    }

所以我们知道,修饰类时,只是表示该类不能被继承,重要的是第四条:修饰对象时,指向地址不可更改,但是数据可以更改。数组是一个对象,可以理解为数组在堆中的地址没有改变,但是里面存储的数据可以改变,不影响数组的地址即可。

final 修饰数组对象,发现数组内数据可以更改,但是因为 private 修饰符的原因,是私有的,外部不可更改,两个修饰符的限定下,char[]不可更改。

    private final char value[];

StringBuilder和StringBuffer 并无限定修饰,所以可变的

    char[] value;

二、StringBuffer

1、StringBuffer定论

StringBuffer 是线程安全的、可变的字符序列、不会增加新的对象

  • 线程安全:StringBuffer使用了关键字 synchronized(这个以后讲,你只要记住它是线程安全的即可) ,该关键字表示同步的,确保在同一个时刻,只有一个线程可以执行某个方法,表现出串行顺序执行似的,保护数据的安全
  • 可变的字符序列:在StringBuffer定义了每个字符串缓冲区都有一个容量。只要字符串缓冲区中包含的字符序列的长度不超过容量,就没有必要分配新的内部缓冲区数组。如果内部缓冲区溢出,它会自动变大
  • 不会增加新的对象:每次对 StringBuffer 对象本身进行操作,而不是生成新的对象

2、StringBuffer构造方法

对于String 既可以使用构造器,也可以直接赋值,但是 StringBuffer只能使用构造器,所以我们先从构造器开始了解
在这里插入图片描述

  • 使用默认构造函数,StringBuffer会默认分配一个16大小的字符缓存区
    public StringBuffer() {
        super(16);
    }
  • 指定大小,根据参数大小分配
    public StringBuffer(int capacity) {
        super(capacity);
    }
  • 初始化内容,字符串缓冲区的初始容量加上 16 大小长度。
    public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }

从上面,大家应该有个概念了,StringBuffer这个类必须创建实例才可使用,不能直接赋值,并且使用默认构造器时,StringBuffer内部定义了一个默认大小为 16 的字符串缓存区,当数据没有超过这个大小时,则不会增加长度,下面直接用常用方法来解析


3、StringBuffer常用方法示例和原理

3.1、常用方法示例

  1. StringBuffer常用方法有两类:append() 和 insert()
  2. 方法 append() 表示可以拼接字符串和各类数据类型,而insert() 可以插入字符。 两者重载了不同参数类型,使其绝大部分数据类型都可以用

append()方法示例:

  • append():表示可以拼接各类数据类型
  • 在这里插入图片描述

代码示例:
代码显示 s1 拼接 s2 使用了 append() 方法,可以拼接String 定义的字符串

    public static void main(String[] args) {
        String s0 = "喜欢美食";
        StringBuffer s1 = new StringBuffer("晚归的生活");
        StringBuffer s2 = s1.append(s0);
        System.out.println(s2);    输出晚归的生活喜欢美食
    }

当然也可以直接拼接

    public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer(10);
        StringBuffer append = s1.append(1);
        System.out.println(append); 输出 1
    }

所以从上面演示可知,append() 重载实现了大部分数据类型,可以拼接不同类型数据,甚至有时候可以用于插入


insert() 方法示例

  • insert():表示在指定的位置插入数据,和append()一样,支持所有的基本数据类型
    在这里插入图片描述
    代码示例:
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer("abcdefg");
        s1.insert(0,1);   索引0处插入1
        System.out.println(s1);  输出1abcdefg
    }

下面我们看一下内部代码的实现

3.2、方法内部实现原理

以方法append()为示例
1、现在定义一个需要初始化的字符串

    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer("abcdefg");
        s1.append("a");
        System.out.println(s1);
    }

2、调用append() 方法,查看其中的源码实现:发现内部是调用了父类 AbstractStringBuilder 的append()

    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);  调用父类的append()
        return this;
    }

3、跳转到父类的append() 方法实现时,它会先调用一个 ensureCapacityInternal() 方法

    public AbstractStringBuilder append(String str) {
        if (str == null)    当字符串为空,则在后面加上null
            return appendNull();
            
        int len = str.length();  获取字符长度
        ensureCapacityInternal(count + len);调用方法,增加长度扩容数组,然后赋值给原变量value
        str.getChars(0, len, value, count);  把字符串复制到扩容后的char[]
        count += len;
        return this;
    }

4、我们继续往下走, 查看 ensureCapacityInternal 内部实现,根据源码说明:minimumCapacity表示最小容量
当所需最小容量大于原本字符组容量时,则调用方法 Arrays.copyOf(value,newCapacity(minimumCapacity));
如果最小容量没有大于原本字符组容量,则什么都不做。

  • 主要是判断添加数据后的数组大小是否超过原本数组大小
    private void ensureCapacityInternal(int minimumCapacity) {
       
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value, newCapacity(minimumCapacity));
        }
    }

5、假如我们插入的数据已经超过了原本所需容量大小,现在需要扩容,调用 Arrays.copyOf(value, newCapacity(minimumCapacity))

  • 主要作用是返回一个符合容量要求的数组

扩展

Arrays.copyOf(char[] original, int newLength) 表示返回一个newLength长度的数组,并拷贝originl内容
original:第一个参数为要拷贝的数组对象
newLength:第二个参数为拷贝的新数组长度

6、我们直接看 newCapacity(minimumCapacity) 方法的内部实现

  • 表明作用是创建一个新的容量大小,注意看返回值
  • 如果新容量小于指定的最小容量,则新容量为指定的最小容量(避免内存浪费)
    private int newCapacity(int minCapacity) {   创建一个新的容量大小
        
        int newCapacity = (value.length << 1) + 2;  新容量为旧容量的两倍加上 2
        if (newCapacity - minCapacity < 0) {   如果新容量小于指定的最小容量,则新容量为指定的最小容量(避免内存浪费)
            newCapacity = minCapacity;
        }
        MAX_ARRAY_SIZE等于Integer.MAX_VALUE - 8,判断新容量是否在0<x<MAX_ARRAY_SIZE之间,是则返回,否则判断一下是否溢出
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)  
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

7、好了,此时创建了一个符合要求的数组了,再返回 ensureCapacityInternal(),后续调用 getChars() 方法

  • 主要作用将字符串中的字符复制到目标字符数组中,并返回 this 这个对象
    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;
    }

扩展

返回值方法作用
voidgetChars​(int srcBegin, int srcEnd, char[] dst, int dstBegin)将字符从此序列复制到目标字符数组 dst

srcBegin – 要复制的字符串中第一个字符的索引。
srcEnd – 要复制的字符串中最后一个字符之后的索引。
dst – 目标数组。
dstBegin – 目标数组中的起始偏移量。

上面大致说明了 StringBuffer中拼接方法append() 的整体流程,其他方法也差不多如此,有兴趣的查看源码即可


3.3、总结

当需要拼接字符串时,需要调用 append() 方法,其内部实现了 ensureCapacityInternal(int minimumCapacity) 方法,判断该操作是否需要扩容,有两种情况,扩容、不扩容

  • 扩容:如果指定的最小容量大于当前容量,则旧容量需要扩容 2 倍加2
  • 不扩容: 如果指定的最小容量小于等于当前容量,则不会进行扩容。

最后调用 getChars() 方法实现数组的复制


三、 StringBuilder

  • StringBuilder 是可变字符序列
  • 适合单线程情况下,属于线程不安全的
  • StringBuilder 内部实现和StringBuffer差不多,所以这不多讲了

扩展一下:

当我们遇见加号拼接时:字符串 + 字符串,在编译期间,字符串会被优化为

new StringBuilder().append("字符串1").append("字符串2").toString();

查看一下 toString() 方法,返回的还是一个字符串

public String toString() {
    return new String(value, 0, count);
}


四、 String和StringBuffer、StringBuilder差异

  • String 是不可变的,而StringBuffer、StringBuilder属于可变序列字符类,两者只需要扩容底层数组大小即可
  • String 可直接赋值和使用构造函数,而 StringBuffer、StringBuilder 只能使用构造函数
  • StringBuffer 适合多线程下,线程安全的,但是效率低些,因为加了 synchronized 关键字
  • StringBuilder适合单线程,线程不安全,但是速度快
  • 39
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值