在上一篇文章中,我们学习了String类中的常用方法,不知道大家是否留意到我反复提到的一句话:修改和截取时,返回的都是新的字符串,原先的字符串并没有改变。
那么这篇文章,我们就来看一下我为什么反复的提这句话。当然,如果没有看的同学可以先看一下我的上一篇文章:Java基础总结(六):String类中的常用方法
从源码分析String为什么不可变:
首先,我们知道,String的本质是不可变字符序列,听名字就知道了哈,他本身就是没有办法更改的,我们要对他做一些改变的话,只能创建一个新的字符串,而不能修改它本身。我们通过源码来分析一下他为什么不能被更改:
可以看到,String的本质其实是一个Char类型的数组,在他的源码中我们可以清楚的看到这个数组前面使用final进行了修饰,我们知道一旦被final修饰,就表明他不再能够被更改了。所以,String是不可变的字符序列。
使用String的弊端和改进方法:
那么有人就说了,不对啊,我不止一次的更改过字符串啊,比如我们常见的拼串,不就是改变了字符串吗?其实我们只是创建了新的字符串而已,原先的字符串并没有改变。
这样做有一个弊端,就是我们每操作字符串一次,就会生成一个新的字符串对象,也就会占据一个对象的内存空间,这会造成内存空间的浪费,当然,我们现在的电脑内存已经很大了,多几个字符串的内存空间不算什么,但不要忘了,我们目前的程序也好,项目也好,大多都是自己运行,自己使用,不会出现多用户的操作。但如果像企业中开发的那些项目,同时几千几万甚至几十万人在线操作,举一个很简单的例子:
王者荣耀这款游戏的日活跃用户最高达到了1亿以上,如果大家都更改自己的游戏昵称,在使用String的情况下,更改昵称会产生大量的新字符串,这对服务器是一个很大的考验。
那么怎样避免这种情况发生呢?这就需要用到我们的可变字符序列:StringBuilder和StringBuffer了。
StringBuilder和StringBuffer:
他俩都是可变字符序列,不同的是:StringBuffer实现了线程安全,需要做线程同步检查,StringBuilder不实现线程安全,不用做线程同步的检查。相比之下StringBuilder的效率就会略高一些,现阶段我们没有开发多线程的项目,所以一般采用StringBuilder。
通过源码了解StringBuilder和StringBuffer:
首先我们同过他们的源码了解一下他们,顺便提一下,通过源码学习是一种很好的习惯,能够帮助我们更好的理解一些工具类的工作原理,同时我们也可以学习开发者高效的代码结构和逻辑,这在无形中就会提高我们自己的开发效率。在IDEA中,我们按住Ctrl键,点击代码中的String即可阅读String类的源码啦。
StringBulider源码:
父类构造方法:
首先我们可以看到,StringBuilder继承自AbstractStringBuilder,同时它的构造方法中调用了父类的构造方法,在父类构造方法中可以看到,他的本质依然是一个char类型的数组,不同的是前面不再有final修饰,也就代表它是可以改变的。
StringBuffer源码:
我们可以发现,StringBuffer同样继承自AbstractStringBuilder,这也就表明了它和StringBuilder的方法是完全相同的,不同的是,在StringBuffer的方法前都有synchronized关键字修饰,这就是线程同步的关键字,表明StringBuffer需要做线程同步检查。
了解了源码之后,我们再来学习一下StringBuilder中的一些常用方法(由于StringBuffer和StringBuilder方法相同,我们只了解其中一个即可):
append(String str) | 在原先字符串后追加str |
insert(int index,String str) | 在原先字符串index的位置插入str |
delete(int beginindex,int endsindex) | 删除原先字符串中beginindex到endsindex位置的字符 |
reverse() | 将原先字符串逆向排列 |
下面使用代码演示:
StringBuffer sb1=new StringBuffer();
sb1.append("我爱我的祖国");
System.out.println("初始可变字符序列:"+sb1);
//追加“,祖国河山大好。”
sb1.append(",祖国河山大好");
System.out.println("追加后的字符串:"+sb1);
//删除0到2的字符
sb1.delete(0,2);
System.out.println("删除后的字符串:"+sb1);
//插入“美丽”
sb1.insert(1,"美丽");
System.out.println("插入后的字符串:"+sb1);
//将字符串逆序
sb1.reverse();
System.out.println("逆序后的字符串:"+sb1);
运行结果:
可以看到,从始至终我们都是对sb1这个字符串在操作,并没有新的字符串生成,这就是可变字符串好用的地方。
用代码比较String和StringBuilder所占用的内存和效率:
接下来我们使用代码来看看,在大量更改字符串时,String和StringBuilder效率的对比:
注释我都写在代码里了:
//String 与StringBulider 效率对比:
System.out.println("首先使用String======================================");
//创建字符串
String str1="";
//返回JVM当前剩余的内存空间,单位为字节
long num1= Runtime.getRuntime().freeMemory();
//返回当前的时间
long time1=System.currentTimeMillis();
//对str1进行10000次更改
for(int i=0;i<10000;i++){
str1+=i;//str1=str1+1
}
//返回大量更改字符串工作完成后的时间和剩余内存
long num2=Runtime.getRuntime().freeMemory();
long time2=System.currentTimeMillis();
//用结束时间和内存减去开始前的时间和内存,就是完成工作所消耗的时间和内存
System.out.println("String占用的内存:"+(num2-num1));
System.out.println("String占用的时间:"+(time2-time1));
System.out.println("下面使用StringBuilder===============================");
StringBuilder sb=new StringBuilder();
//返回JVM剩余的内存空间,单位为字节
long num3= Runtime.getRuntime().freeMemory();
// 返回当前时间,注意单位是毫秒
long time3=System.currentTimeMillis();
for(int i=0;i<10000;i++){
sb.append(i);
}
//同样返回结束时的时间和内存,与开始前的时间和内存相减即可得到占用的时间和内存
long num4=Runtime.getRuntime().freeMemory();
long time4=System.currentTimeMillis();
System.out.println("StringBuilder占用的内存:"+(num4-num3));
System.out.println("StringBuilder占用的时间:"+(time4-time3));
原理就是先获得刚开始的内存和时间,然后获取完成工作后的内存和时间,两个相减即可得到完成工作所占用的时间和内存。我们同样对字符串进行10000次的更改,看看会有什么差别
运行效果:
可以看到,对比相当的明显,由于String每次都产生新的对象,所以对内存的消耗可以用恐怖来形容,而StringBuilder消耗的内存几乎可以忽略不计,因为从始至终只产生一个对象。消耗时间方面,由于没次都要开辟新的内存空间,所以String更慢。
总结:
这篇文章总结了可变字符序列和不可变字符序列的区别,简单的带大家了解了StringBuilder和StringBuffer,并通过对比直观的感受了二者效率的差异,以往大家能够真正的理解他们的区别。