提示:文章写完后,目录可## 标题以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
提示:以下是本篇文章正文内容,下面案例可供参考
一、使用StringBuilder或StringBuffer的目的
为什么有了String 还要去学习去使用StringBuilder呢? 这里面就涉及到了String类型的特性了
1.String类型的不可变性
1.1 value数组
我们先新建一个String 类型对象 去看看String类型的构造方法
String str1=new String("字符串");
发现这个构造方法完成了两个赋值操作;
这个value属性 和hash 属性分别是什么?
原来是一个被final修饰的char[]类型数组 名称为value;
以及一个int类型的hash值(这个涉及到字符串常量池,感兴趣的可以去看一下)。
这里我们重点关注一下这个value数组;
再new String();的时候 我们其实就可以传入一个char[]类型的数组进去
String str3=new String(new char[]{'字','符','串'});
之前的代码块里面String str1=new String(“字符串”),相当于把 字符串常量“字符串”内部value数组的引用赋值给了str1对象的value数组
而这个传递字符类型数组创建参数的时候 是把这个字符数组的引用值赋值过去了;
并且这个value数组被final修饰,因此也无法改变value数组空间的指向,不能再修改value指向的地址 但是指向空间的元素其实是可以改变的
如果我们创建一个char类型数组 将这个char类型数组的引用传递进去 然后通过char数组改变其指向空间的元素值 这个时候不就在吧改变value空间指向的情况下改变了String的内容吗?
先看结果
char[] cArray=new char[]{'字','符','串'};
String str3=new String(cArray);
cArray[0]='子';
System.out.println(Arrays.toString(cArray));
System.out.println(str3);
}
运行结果:
似乎并没有修改成功,为什么呢?
我们去源码一探究竟
原来他在底部调用了Arrays.copyOf方法,这个方法的返回值也是一个数组,传入的参数是一个模板数组,一个是返回数组的长度。
该方法是在底部创建一个新的数组,然后调用System.arraycopy
方法 将模板数组的元素值赋给新建的数组 然后将这个数组返回;
所以我们刚才没有修改成功的原因找到了,当你传进来数组的时候,我们是新创了一个数组 已经和那个char[]类型数组不是同一片空间了,变成一个别人无法访问修改的private value数组;
所以这就是String类型具备不可变性的重要因素:
value被final修饰 指向空间不可变;
同时也被private 修饰 我们无法通过使用str.value更何况value[0] 改变其内容呢?
1.2 String方法
这也就是为什么我们使用toUpperCase()
、replace()
方法 都无法改变调用这个方法的str对象自身的内容:
str3.toUpperCase();
str3.replace('字', '子');
System.out.println(str3);
输出:
并没有改变,我们去看看源码
返回值类型是String 里面参数合法的情况下 会返回一个新的字符串 return new String()
所以这意味着我们需要接受一下这个新生成的字符串才有用,
而原来的str3 并不会发生改变;
String下其余几个方法 需要返回值的 需要声明变量接受的 也都是返回一个新创的字符串,不会改变源字符串;
concat方法:返回的也是新的 其余方法也是;
1.3 不可变性
正是因为String类型底层的value数组被private finals修饰
并且其他生成String、返回的方法大都是生成一个新的字符串
(以常量字符串为参数的构造方法除外 是直接赋值了value数组的引用 不是新生成一个value数组 但是因为他是常量 不会改变 所以也没有新建value数组然后private隐藏的必要)
1.4 开销问题
假如我有这样一个需求,将一个数组中的所有元素拼接到字符串上,我们怎么做?
:
int num[]=new int[10];
for (int i = 0; i < num.length; i++) {
str5=str5.concat(String.valueOf(i)+" ");//把int i 转成字符串形式拼接上去
// str5=str5+i+" ";//相当于使用 + 把int类型也转成了字符串形式 ;
}
System.out.println(str5);
我们细数一下 创建了几次(上文提到过concat()方法是创建了新的对象)
concat拼接了10次
String.valueOf()生成了10次
“ ”这个空格字符串创建了内容为空格的常量字符串 相当于生成了1次
+“ ” 这一操作 相当于把String.valueOf()生成的字符串 拼接了一个空格
每次一拼接 相当与生成了一个新的字符串 这个字符串是 + 拼接后的结果
+这一操作生成了10次
所以
仅仅是这样一个简单的需求 因为String自身的不可变性 每发生一点内容的改变 就要创建一次新的String对象 这样下来创建了三十一次对象 ,开销实在太大。
能否有一种类型 可以直接在其本身操作,而不必新建对象?
有,答案就是StringBuilder 与 StringBuffer
二、StringBuilder与StringBuffer
写到这儿突然肚子饿了 只能草草写完收场去吃饭。见谅
StringBuilder StringBuffer的底层也是用char类型的value数组存储字符串的(默认长度是16)
但是区别于String 这个value数组的指向 是可以变的 ;
StringBuilder源码:
AbstractStringBuilder抽象类源码:
1.StringBuilder与StringBuffer的创建
三种构造方法(这里我们不看以CharSequence seq为参数的那个 这是个接口 我们就先把他String类型看好了)
StringBuilder sb1=new StringBuilder(); //注意默认容量为16
StringBuffer sb2=new StringBuffer();
//传入int 容量
StringBuffer sb1_1=new StringBuffer();
StringBuilder sb2=new StringBuilder(3);
StringBuffer sb2_1=new StringBuffer(3);
//传入 String
StringBuilder sb3=new StringBuilder("我爱学习吗?");
StringBuffer sb3_1=new StringBuffer("不你真不爱");
这两个在创建以及操作上大同小异,因此我们后续将使用其中一个作为示范。
2.同样的拼接操作;
还是同样的需求,将一个数组中的所有元素拼接到字符串上;
注意:StringBuilder 拼接使用append方法 不能像字符串那样直接+ 会报错
这涉及到java底层(底层是C++)有关运算符重载的问题 如果对这个问题感兴趣,可以去搜搜C++中的运算符重载。
append方法:
里面重载了很多方法,拼接时候可以直接将int类型值传进去 他会在底部将其转换为字符串拼接到StringBuilder上。
看一下源码:
这时候我们发现 他的返回值并不像String那样 new一个新的对象,而是返回当前调用这个方法的对象本身;也就是说 拼接完了 我并不是返回一个新的 而是返回自身;
具体看一下
StringBuilder sb1=new StringBuilder();
sb1.append(5).append(" ").append(6);
//因为返回值也是StringBuilder类型 所以可以这样链式操作
System.out.println(sb1);
结果:
那么回到最初的需求:
StringBuilder sb1=new StringBuilder(); //注意默认容量为16
int num[]=new int[10];
for (int i = 0; i < num.length; i++) {
sb1.append(i).append(" ");
}
System.out.println(sb1);
}
这一次 我们似乎只创建了一次对象 然后再这个对象本身进行的操作。
开销就大大减少
可是回到底层,是这样吗? 是真的只创建了一次对象吗?
有细心的朋友会发现,刚开始我调用无参构造生成StringBuilder时候 他调用父类的构造方法 给我传递了个容量16,
点进super看看
那么问题来了:我起初长度是16 为什么能放至少20个字符呢?这中间发生了什么?、
想要回答这个问题 我们要接着讨论其内部的扩容机制。
3.扩容机制
这里面的三个方法 我们对Integer.stringSize(i)
、 Integer.getChars(i, spaceNeeded, value)
,做个大概介绍 感兴趣的可以去看看源码,把重点放在ensureCapacityInternal(spaceNeeded)
上;
Integer.stringSize(i)
:返回一个int值,该int值是 传入进来要添加,要append的数值i 转成字符存进value[]数组要占多少长度;例如 123存进去就是 要占三位 放三个字符 分别是‘1’ ‘2’ ‘3’;
Integer.getChars(i, spaceNeeded, value)
:该方法中i是传进来的int值,spaceNeeded=count+appendedlength;其中appendedlength是i要占多少长度,count是当前数组的内容长度,spaceNeeded就是放完之后的长度;(提前算出来了 到这一步才开始放); 该方法的作用就是把该int i 作为字符存到数组中count位置的后面;
其中spaceNeeded的作用是帮助底层更方便的存储数字,因为假如要存12345 那么用字符存进去就是‘1’,’2‘,’3‘,’4‘,’5‘;它底层是先存’5‘ ’5‘的这个位置就是spaceNeeded提供的 接着spaceNeeded-- 一次存储’4‘,’3‘,’2‘,’1‘;
接下来就要看ensureCapacityInternal(spaceNeeded)
再看之前 我们先接受两个方法,方便我们更直观感受 在扩容时候容量和内容长度的变化;
源码中一个返回StringBuilder的当前内容长度 public int length()
,一个返回当前容量的方法public int capacity()
;
看一下ensureCapacityInternal(spaceNeeded)
的底层实现
里面进行了一个判断,如果传进来的minimumCapacity
最小需要容量 也就是之前当前内容长度+传进来内容长度赋值的spaceNeeded
;
那么这种情况就意味着容量不够 我们需要扩容了
Arrays.copyOf(value,newCapacity(minimumCapacity))
:该方法会返回一个char类型的数组,传进来的两个参数,前一个参数是模板数组,后面的方法返回的int类型值是最终返回数组的长度;底部是新建了一个指定长度的数组 ,并且将value里面的元素全都装到返回的char类型数组上(就是遍历模板数组然后给新创建的这个数组元素赋值);
newCapacity(minimumCapacity))
是扩容确定长度的关键:他会按照需求给出你要的这个容量长度,这个传进去的minimumCapacity是你的需求,返回值是他实际需要设置的容量值;
看看底层:
进去之后位运算左移两位,相当于乘2;再加2;
再做一个比较:如果我乘了两倍+2 还是不够满足你需求,不够满足你minCapacity的需求,那么这次扩容就扩到这个的minCapacity长度;
之后就返回newCapacity(中间return语句是一个lambda表达式是观察有没有溢出 感兴趣去看 )初学者就当它返回了newCapacity 就好;
所以现在看一下下面的操作:
插入了20个元素 实际上容量是34;
34怎么来的? 在没插到16个之前都不需要扩容;
再插到第17个的时候就需要扩容了 他的具体变化就是 16*2+2=34;
再来看一下,乘2 加2 也满足不了需求容量的情况:
int num[]=new int[10];
for (int i = 0; i <10; i++) {
sb1.append(i).append(" ");
}
//这时候 内容长度是20
//容量扩充到34了 34-20/2=7
for (int i = 0; i<7; i++) {
sb1.append(i).append(" ");
}
//输出一下 length capacity 都是34
//这时候如果再append 1个元素 就是扩充为34*2+2=70 70-34=36
//那一次插入37个
char c[]=new char[37];
Arrays.fill(c, 'A');;
sb1.append(c);
//这时候再输出一下 内容长度 和容量 应该是71
System.out.println(sb1.length());
System.out.println(sb1.capacity());
System.out.println(sb1);
sb1.length();
//输出果然是 71 71
4.两者的区别
1.版本
- StringBuffer是jdk1.0版本就有的
- StringBuilder是jdk1.5添加的
2.线程安全性:
- StringBuffer:是线程安全的。这意味着在多线程环境中,多个线程可以安全地访问和修改同一个StringBuffer实例,而不会导致数据不一致的问题。这是因为StringBuffer的所有公开方法都是使用synchronized关键字修饰的,从而确保了线程安全性。
- StringBuilder:是线程不安全的。在多线程环境中,如果有多个线程同时访问和修改同一个StringBuilder实例,可能会导致数据不一致或其他并发问题。因此,在需要线程安全性的场合,应使用StringBuffer而不是StringBuilder。
3.性能:
- 由于StringBuilder没有同步机制,因此它的性能通常优于StringBuffer。在单线程环境中,特别是在需要频繁进行字符串修改(如追加、插入、删除等)的场景下,使用StringBuilder可以显著提高性能。
- StringBuffer的每次toString操作都会直接使用其内部的缓存区来构造字符串,但由于其同步机制,这一过程仍然是同步的。而StringBuilder在每次需要构造字符串时都需要复制一次字符数组,这在一定程度上影响了其性能,但在单线程环境下,这种影响通常可以忽略不计。
4.使用场景:
- StringBuffer:适用于多线程环境中需要频繁修改字符串的场景。由于它提供了线程安全性,因此可以在多个线程之间安全地共享和修改字符串数据。
- StringBuilder:适用于单线程环境中需要频繁修改字符串的场景。由于其高性能和易用性,它已成为Java中处理字符串的首选工具之一。