StringBuilder与StringBuffer详解

提示:文章写完后,目录可## 标题以自动生成,如何生成可参考右边的帮助文档


前言

提示:以下是本篇文章正文内容,下面案例可供参考

一、使用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方法:返回的也是新的 其余方法也是;
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/49c63511d74b4d8da11814addfd0eacd.png

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中处理字符串的首选工具之一。
  • 27
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值