注:学 StringBuffer、StringBuilder 之前最好对 String 比较了解,要不然会比较吃力
参考笔记:java StringBuilder 和 StringBuffer 万字详解(深度讲解)_java stringbuffer和stringbuilder-CSDN博客
前置内容:
【Java SE】Java中String的内存原理-CSDN博客
目录
3.StringBuffer VS String(非常重要)
3.1 StringBuffer类与String类保存字符串的比较
3.2 StringBuffer类与String类的相互转换
9.String、StringBuffer、StringBuilder对比
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 的父类 AbstractBuilder 的 appendNull 函数中将 'n'、'u'、'l'、'l' 存入到 char[ ] value 数组中,因此 sf.length() 即 StringBuffer 容器中字符串的有效长度输出为 4, System.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 方法:
可以看到, StringBuffer 与 StringBuilder 的方法实现唯一的区别就是是否有 synchronized 关键字,所以是 StringBuffer 是线程安全的, StringBuilder 是线程不安全的
StringBuffer的继承关系图
我使用的是 JDK 8.0 ,StringBuilder 继承关系图与 StringBuffer 唯一不同的是少了实现接口 Comparable ,好像更高版本的 JDK 就有实现该接口
6.2 溯源
StringBuilder 类的源码,如下:
StringBuilder 同样继承父类 AbstractStringBuilder 的 char[ ] 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 : 适用于存在大量修改字符串的情况,且满足单线程条件