【Java SE】StringBuffer、StringBuilder详解

注:StringBuffer、StringBuilder 之前最好对 String 比较了解,要不然会比较吃力

参考笔记:java StringBuilder 和 StringBuffer 万字详解(深度讲解)_java stringbuffer和stringbuilder-CSDN博客

前置内容:

【Java SE】Java中String的内存原理-CSDN博客

【Java SE】String类详解-CSDN博客


目录

1.StringBuffer的介绍和溯源

1.1 介绍

1.2 溯源

2.StringBuffer类常用的构造器

3.StringBuffer VS String(非常重要)

3.1 StringBuffer类与String类保存字符串的比较

3.2 StringBuffer类与String类的相互转换

3.2.1 StringBuffer -> String

3.2.2 String -> StringBuffer

4.StringBuffer类常用方法

5.StringBuffer练习题

5.1 练习题1

5.2 练习题2

6.StringBuilder的介绍和溯源

6.1 介绍

6.2 溯源

7.StringBuilder类的常用构造器

8.StringBuilder类的常用方法

9.String、StringBuffer、StringBuilder对比

9.1 特性对比

9.2 效率对比测试

9.3 使用场景


1.StringBuffer的介绍和溯源

1.1 介绍

String 类中,每个字符串对象都是常量。当创建一个字符串对象,之后对其内容进行 "增删改" 时,实际上原来的字符串对象已经丢弃了。JVM 在底层会重新创建一个字符串对象,并令其指向新的数据空间

所以,问题就来了:如果多次进行  "增删改"  的操作,会导致大量副本字符串对象遗留在内存中,降低效率。应该如何解决这个问题呢?这便引出了 StringBuffer

        StringBuffer 类,指可变字符序列,用于构造字符串对象。其内部使用自动扩容的数组来存放字符串数据StringBuffer 类属于 java.lang 包下,其继承关系图如下:

可以看到, StringBuffer 类并没有像 String 类一样直接继承 Object 类,而是直接继承 AbstractStringBuilder 类。它也像 String 类一样实现了两个关键的接口:

① Serializable接口:实现该接口使得 StringBuffer 类型可串行化,串行化后,StringBuffer 类型可以进行网络传输

② Comparable接口:实现该接口使得 StringBuffer 类型的对象可以进行“比较”的操作 

1.2 溯源

 StringBuffer 类的源码,如下:

String 类,StringBuffer 类也用 final 关键字修饰,因此,StringBuffer 类也不可被继承

但是,看到这可能很多人会很疑惑:在 String 类的源码中,明明白白地可以看到由 "private final char[ ] value" 属性,并且源码中给出了注释—字符串在底层就是用这个字节数组来存储的,如下:

但在 StringBuffer 类中似乎并没有看到类似的 char 类型数组,这时候就可以去看看 StringBuffer 的父类 AbstractStringBuilder 的源码:

可以看到,父类 AbstractStringBuilder 源码中有 char[] value 属性,并且源码中也明确给出了注释 "The value is used for character storage."。但与 String 类不同的是,该数组没有用final修饰!  因此,StringBuffer 字符串实际存放的位置是在 堆内存 中。这也从根本上解释了为什么 StringBuffer 是可变字符序列

"StringBuffer字符串实际存放的位置是在堆内存中" 的验证

验证方法 ①:通过构造器 "StringBuffer(String str)" 来初始化一个非空的 StringBuffer 类对象时,在底层会对 value 有一个 new 的操作(如下图)。而 new 出来的对象都是在 堆内存 中的 

可以看到,使用 "StringBuffer(String str)" 构造器的时候,底层开辟的 char[ ] value 数组的长度是:str.length( ) + 16


验证方法 ②:通过空参构造器  "StringBuffer( )" 初始化 StringBuffer 类对象,底层会通过 "super(16)" 调用父类的一个带参构造,为 value 属性开辟一个长度 16 的字符数组空间

因此,使用空参构造初始化 StringBuffer 类对象时,底层的 char[ ] value 数组默认初始容量 = 16

2.StringBuffer类常用的构造器

① StringBuffer():

        构造一个不带字符的字符串缓冲区(char[ ] value),其初始容量为 16 个字符

② StringBuffer(int capacity):

        构造一个不带字符,但具有指定初始容量的字符串缓冲区。即将 char[ ] value 的大小指定为 capacity

③ StringBuffer(String str):

        构造一个字符串缓冲区,并将其内容初始化为指定字符串的内容。底层 char[ ] value 的大小是 str.length() + 16

 案例

public class demo {
    public static void main(String[] args) {
        //演示 : 演示StringBuffer类的常用构造器
        //1.StringBuffer()
        StringBuffer stringBuffer_0 = new StringBuffer();
        System.out.println(stringBuffer_0.length());
        System.out.println(stringBuffer_0);
        System.out.println("----------");

        //2.StringBuffer(int capacity)
        StringBuffer stringBuffer_1 = new StringBuffer(141);//开辟的char[] value长度为141
        System.out.println(stringBuffer_1.length());
        System.out.println(stringBuffer_1);
        System.out.println("----------");

        //3.StringBuffer(String str)
        StringBuffer stringBuffer_2 = new StringBuffer("唱跳rap篮球");
        System.out.println(stringBuffer_2.length());
        System.out.println(stringBuffer_2);
    }
}

Debug演示 

① 第一个构造器Debug演示GIF图如下 : 

② 第二个构造器Debug演示GIF图如下 :

③ 第三个构造器Debug演示GIF图如下 :

3.StringBuffer VS String(非常重要)

3.1 StringBuffer类与String类保存字符串的比较

① String 类保存的是字符串常量,无法直接更改字符串本身的值。而且 String 类的每次更新实际上就是创建新的字符串对象,然后更改引用指向的地址,效率较低,空间消耗也比较高

public class demo{
    public static void main(String[] args) {
        String str = new String("唱跳rap");
        str = new String("666");
        str = "小马";
    }
}

内存原理图 

验证:"String类的每次更新实际上就是创建新的字符串对象,然后更改引用指向的地址"

public class demo{
    public static void main(String[] args) {
        String str = new String("唱跳rap");
        System.out.println("\"唱跳rap\"的地址对应的hashCode:"+str.hashCode());
        str = new String("666");
        System.out.println("\"666\"的地址对应的hashCode:"+str.hashCode());
        str = "小马";
        System.out.println("\"小马\"的地址对应的hashCode:"+str.hashCode());
    }
}

运行结果:


  StringBuffer 保存的是字符串变量,可以直接更改字符串的内容。因为字符串变量在堆内存中,StringBuffer 的每次更新实际上是直接更新字符串的内容,不会创建新的对象,所以不用每次更新引用指向的地址,效率较高。只有在某些特殊情况下,比如说 char[ ] value 数组预存的空间不足,需要扩容时(StringBuffer有扩容机制),才会创建新的对象

public class demo{
    public static void main(String[] args) {
        StringBuffer sf = new StringBuffer("csdn");
    }
}

 内存原理图 

验证:"StringBuffer的每次更新实际上是直接更新字符串的内容,不会创建新的对象,所以不用每次更新引用指向的地址"

public class demo{
    public static void main(String[] args) {
        StringBuffer sf = new StringBuffer("csdn");
        System.out.println("更改前的sf:"+sf+",更改前的hashCode:"+sf.hashCode());
        sf = sf.replace(0,4,"yyds");
        System.out.println("更改的后sf:"+sf+",更改后的hashCode:"+sf.hashCode());
    }
}

运行结果: 


3.2 StringBuffer类与String类的相互转换

3.2.1 StringBuffer -> String

方法 ① :利用构造器 StringBuffer (String str)

方法 ② :使用无参构造 StringBuffer()  创建一个 StringBuffer 对象,再利用 append 方法向容器中添加字符(串)

演示 

public class demo{
    public static void main(String[] args) {
        //演示String -> StringBuffer
        //方式一
        String str1 = "唱跳rap篮球";
        StringBuffer sf1 = new StringBuffer(str1);
        System.out.println(sf1);
        
        //方式二
        StringBuffer sf2 = new StringBuffer();
        sf2.append(str1);
        System.out.println(sf2);

        System.out.println(sf1==sf2);//false
    }
}

3.2.2 String -> StringBuffer

方法 ① :StringBuffer 类的 toString 方法

方法 ② :String 类构造器 String(StringBuffer buffer)

演示 

public class demo{
    public static void main(String[] args) {
        //演示StringBuffer -> String
        //方式一
        StringBuffer sf = new StringBuffer("鸡你太美");
        String str1 = sf.toString();
        System.out.println(str1);
        
        //方式二
        String str2 = new String(new StringBuffer("小马唱跳rap"));
        System.out.println(str2);
    }
}

4.StringBuffer类常用方法

StringBuffer 类实现的方法很多,本文只讲几个比较常用的

① int length():

        获取到当前 StringBuffer 容器(char[ ] value)中字符串的有效长度

② int capacity():

         返回当前容器的容量

③ StringBuffer append(...):

        将传入的形参对应的字符串加入到当前容器中。返回值为 StringBuffer 类型,可不作接收

④ StringBuffer delete(int start,int end):

        删除当前容器中指定位置的内容。传入的两个形参代表了删除的索引区间:[start, end) 返回值为 StringBuffer 类型,可不作接收

⑤ StringBuffer replace(int start,int end,String str):

        将当前容器中指定序列部分的字符串替换为传入的 str 字符串。替换区间是 [start,end)。返回值为 StringBuffer 类型,可不作接收

⑥ StringBuffer reverse():

        将当前容器中的字符串反转顺序后再返回。返回值为 StringBuffer 类型,可不作接收

⑦ StringBuffer insert(int offset,String str):

        在当前容器中字符串的索引 offset 处插入一段字符串,原字符串中的内容从该索引处自动后移。返回值为 StringBuffer 类型,可不作接收

演示 

public class demo {
    public static void main(String[] args) {
        //演示 : StringBuffer类常用方法
        //1 —— int length()
        StringBuffer strBuffer_0 = new StringBuffer("CSDN yyds!");
        System.out.println("当前字符串 = " + strBuffer_0);
        System.out.println("当前容器中字符串的有效长度为:" + strBuffer_0.length());
        System.out.println("============================================");

        //2 —— int capacity()
        StringBuffer strBuffer_1 = new StringBuffer(141);
        System.out.println("当前容器的容量是:" + strBuffer_1.capacity());
        System.out.println("============================================");

        //3 —— StringBuffer append(...)
        StringBuffer strBuffer_2 = new StringBuffer("大家好,");
        strBuffer_2.append("我是练习时长两年半的练习生——");
        strBuffer_2.append("小马——");
        strBuffer_2.append(6666);
        strBuffer_2.append(2333.333333);
        System.out.println("strBuffer_2容器中字符串的内容 = " + strBuffer_2);
        System.out.println("============================================");

        //4 —— StringBuffer delete(int start, int end)
        StringBuffer strBuffer_3 = new StringBuffer("小米,小红,小兰,小黑");
        System.out.println("当前字符串 = " + strBuffer_3);
        strBuffer_3.delete(0, 3);
        System.out.println("删去索引为[0, 3)的字符串后,现在的字符串 = " + strBuffer_3);
        System.out.println("============================================");

        //5 —— StringBuffer replace(int start, int end, String str)
        StringBuffer strBuffer_4 = new StringBuffer("大白 大黄 大哥 大狗");
        System.out.println("当前字符串 = " + strBuffer_4);
        strBuffer_4.replace(9, 11, "大猫");
        System.out.println("将\"大狗\"替换成\"大猫\"后,现在的字符串 = " + strBuffer_4);
        System.out.println("============================================");

        //6 —— StringBuffer reverse()
        StringBuffer strBuffer_5 = new StringBuffer("123456789");
        System.out.println("当前字符串 = " + strBuffer_5);
        strBuffer_5.reverse();
        System.out.println("颠倒字符串的顺序后,现在的字符串 = " + strBuffer_5);
        System.out.println("============================================");

        //7 —— StringBuffer insert(int offset, String str)
        StringBuffer strBuffer_6 = new StringBuffer("我喜欢吃水果");
        System.out.println("当前字符串 = " + strBuffer_6);
        strBuffer_6.insert(1, "小马");
        System.out.println("在索引为1处插入一段字符串后,现在的字符串 = " + strBuffer_6);
    }
}

5.StringBuffer练习题

5.1 练习题1

判断输出结果

public class demo {
    public static void main(String[] args) {
        String str = null;
        StringBuffer sf = new StringBuffer();
        sf.append(str);
        System.out.println(sf.length());
        System.out.println(sf);
    }
}

运行结果:

结果解析

由上图中的源码追溯可以看到,最终是在 StringBuffer 的父类 AbstractBuilderappendNull 函数中将 'n'、'u'、'l'、'l' 存入到 char[ ] value 数组中,因此 sf.length()StringBuffer 容器中字符串的有效长度输出为 4System.out.println(sf) 输出字符串 "null"

5.2 练习题2

判断下列代码会发生什么异常

public class demo {
    public static void main(String[] args) {
        String str = null;
        StringBuffer sf = new StringBuffer(str);
        System.out.println(sf);
    }
}

运行结果:

结果解析

由上图的源码追溯可以看到,使用 StringBuffer(String str) 构造器时,内部的 str.length() 发生空指针异常 

6.StringBuilder的介绍和溯源

6.1 介绍

StringBuffer 一样, StringBuilder 类也是一个可变的字符序列StringBuilder 类提供与 StringBuffer 类兼容的API,因此两者在使用功能上非常相似,但是 StringBuilder 类不保证同步,因此 StringBuilder 类是线程不安全

StringBuilder 类被设计用作 StringBuffer 类的一个简易替换。因为在单个线程使用字符串缓冲区时(或者说在单线程的前提下),StringBuilder 类效率比 StringBuffer 类更高。因此,在满足单线程的基础上,建议优先使用 StringBuilder 类 

 对比 StringBuffer、StringBuilder 的一些方法

append 方法: 

replace 方法:

可以看到, StringBufferStringBuilder 的方法实现唯一的区别就是是否有 synchronized 关键字,所以是 StringBuffer 是线程安全的, StringBuilder 是线程不安全的

StringBuffer的继承关系图

我使用的是 JDK 8.0 ,StringBuilder 继承关系图与 StringBuffer 唯一不同的是少了实现接口 Comparable ,好像更高版本的 JDK 就有实现该接口

6.2 溯源

StringBuilder 类的源码,如下:

StringBuilder 同样继承父类 AbstractStringBuilderchar[ ] value 字符类型数组

7.StringBuilder类的常用构造器

StringBuilder 类的构造器实现跟 StringBuffer 是完全一样的

① StringBuffer():

        构造一个不带字符的字符串缓冲区(char[ ] value),其初始容量为 16 个字符

② StringBuffer(int capacity):

        构造一个不带字符,但具有指定初始容量的字符串缓冲区。即将 char[ ] value 的大小指定为 capacity

③ StringBuffer(String str):

        构造一个字符串缓冲区,并将其内容初始化为指定字符串的内容。底层 char[ ] value 的大小是 str.length() + 16

演示 

public class demo {
    public static void main(String[] args) {
        //演示 : 演示StringBuilder类的常用构造器
        //1.StringBuilder()
        StringBuilder stringBuilder_0 = new StringBuilder();
        System.out.println(stringBuilder_0.length());
        System.out.println(stringBuilder_0);
        System.out.println("----------");

        //2.StringBuilder(int capacity)
        StringBuilder stringBuilder_1 = new StringBuilder(141);//开辟的char[] value长度为141
        System.out.println(stringBuilder_1.length());
        System.out.println(stringBuilder_1);
        System.out.println("----------");

        //3.StringBuilder(String str)
        StringBuilder stringBuilder_2 = new StringBuilder("唱跳rap篮球");
        System.out.println(stringBuilder_2.length());
        System.out.println(stringBuilder_2);
    }
}

8.StringBuilder类的常用方法

StringBuilder 类的常用方法的功能和 StringBuffer 是完全一样的,只是少了 synchronized 修饰,是线程不安全的。这里就不再赘述了

9.String、StringBuffer、StringBuilder对比

9.1 特性对比

  • String : 不可变字符序列,一"更新"字符串就会产生一个新的字符串对象,多次执行"更新"操作会使得大量字符串对象留在内存中降低效率,但是复用率高

  • StringBuffer : 可变字符序列,可以直接更改字符串本身的内容,不会创建新的字符串对象。只有在 char[ ] value 数组预存的空间不足需要扩容时才会创建新的对象效率较高,且线程安全

  • StringBuilder : 可变字符序列,可以直接更改字符串本身的内容,不会创建新的字符串对象。只有在 char[ ] value 数组预存的空间不足需要扩容时才会创建新的对象效率最高,但线程不安全 

9.2 效率对比测试

效率对比结果:StringBuilder > StringBuffer > String 

public class demo {
    public static void main(String[] args) {
        //初始设置
        long startTime = 0L;
        long endTime = 0L;
        String text = "";
        StringBuffer buffer = new StringBuffer("");
        StringBuilder builder = new StringBuilder("");

        //时间对比
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 80000; i++) {
            buffer.append(String.valueOf(i));
        }
        endTime = System.currentTimeMillis();
        System.out.println("StringBuffer的执行时间(ms):" + (endTime - startTime));

        startTime = System.currentTimeMillis();
        for (int i = 0; i < 80000; i++) {
            builder.append(String.valueOf(i));
        }
        endTime = System.currentTimeMillis();
        System.out.println("StringBuilder的执行时间(ms):" + (endTime - startTime));

        startTime = System.currentTimeMillis();
        for (int i = 0; i < 80000; i++) {
            text = text + i;
        }
        endTime = System.currentTimeMillis();
        System.out.println("String的执行时间(ms):" + (endTime - startTime));

    }
}

运行结果 

可以看到,String 的执行时间非常慢,快接近 3000ms 了,而 StringBuffer、StringBuilder 的执行时间只有个位数,效率非常高 

9.3 使用场景

① String : 适用于字符串很少被修改,且被多个对象引用的情况,比如定义数据库的IP信息,配置信息等
② StringBuffer : 适用于存在大量修改字符串的情况,且满足多线程条件
③ StringBuilder : 适用于存在大量修改字符串的情况,且满足单线程条件