碎碎念
这是一道老生常谈的问题了,字符串是不仅是Java中非常重要的一个对象,它在其他语言中也存在。比如C++、VisualBasic、C1//执行对象的初始化方法4:returnpublicstaticvoidmain([]);Code:0:ldcpop出栈引用值,将其(引用)赋值给局部变量表中的变量s13:ldc#3//Stringbbbccc5:astore_26:return}
编译器做了优化Strings2="bbb"+"ccc"会直接被优化为bbbccc。也就是直接创建了一个bbbccc对象。
下面来看s3,s3创建了几个对象呢?是一个还是两个?还是有其他选项?我们使用javap-c来看一下
我们可以看到,s3执行+操作会创建一个StringBuilder对象然后执行初始化。执行+号相当于是执行()操作。所以
Strings3=s1+"bbb";==Strings3=newStringBuilder().app(s1).app("bbb").toString();//()方法也会创建一个StringpublicStringtoString(){//Createacopy,don'tsharethearrayreturnnewString(value,0,count);}
所以s3执行完成后,相当于创建了3个对象。
下面来看s4创建了几个对象,在创建这个对象时因为使用了new关键字,所以肯定会在堆中创建一个对象。然后会在常量池中看有没有aaa这个字符串;如果没有此时还会在常量池中创建一个;如果有则不创建。所以可能是创建一个或者两个对象,但是一定存在两个对象。
说完了String对象,我们再来说一下StringBuilder和StringBuffer对象。
上面的String对象竟然和StringBuilder产生了千丝万缕的联系。不得不说StringBuilder是一个牛逼的对象。String对象底层是使用了StringBuilder对象的app方法进行字符串拼接的,不由得对StringBuilder心生敬意。
不由得我们想要真正认识一下这个StringBuilder大佬,但是在认识大佬前,还有一个大boss就是StringBuffer对象,这也是你不得不跨越的鸿沟。
StringBuffer对象代表一个可变的字符串序列,当一个StringBuffer被创建以后,通过StringBuffer的一系列方法可以实现字符串的拼接、截取等操作。一旦通过StringBuffer生成了最终想要的字符串后,就可以调用其toString方法来生成一个新的字符串。例如
StringBufferb=newStringBuffer("111");("222");(b);
我们上面提到+操作符连接两个字符串,会自动执行toString()方法。那你猜方法会自动调用吗?直接看一下反汇编代码不就完了么?
上图左边是手动调用toString方法的代码,右图是没有调用toString方法的代码,可以看到,toString()方法不像+一样自动被调用。
StringBuffer是线程安全的,我们可以通过它的源码可以看出
StringBuffer在字符串拼接上面直接使用synchronized关键字加锁,从而保证了线程安全性。
最后来认识大佬了,StringBuilder其实是和StringBuffer几乎一样,只不过StringBuilder是非线程安全的。并且,为什么+号操作符使用StringBuilder作为拼接条件而不是使用StringBuffer呢?我猜测原因是加锁是一个比较耗时的操作,而加锁会影响性能,所以String底层使用StringBuilder作为字符串拼接。
我们上面说到,使用+连接符时,JVM会隐式创建StringBuilder对象,这种方式在大部分情况下并不会造成效率的损失,不过在进行大量循环拼接字符串时则需要注意。如下这段代码
Strings="aaaa";for(inti=0;i100000;i++){s+="bbb";}
这是一段很普通的代码,只不过对字符串s进行了+操作,我们通过反编译代码来看一下。
//经过反编译后Strings="aaa";for(inti=0;i10000;i++){s=(newStringBuilder()).app(s).app("bbb").toString();}
你能看出来需要注意的地方了吗?在每次进行循环时,都会创建一个StringBuilder对象,每次都会把一个新的字符串元素bbb拼接到aaa的后面,所以,执行几次后的结果如下
每次都会创建一个StringBuilder,并把引用赋给StringBuilder对象,因此每个StringBuilder对象都是强引用,这样在创建完毕后,内存中就会多了很多StringBuilder的无用对象。了解更多关于引用的知识,请看
这样由于大量StringBuilder创建在堆内存中,肯定会造成效率的损失,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用app()方法手动拼接。
例如
StringBuilderbuilder=newStringBuilder("aaa");for(inti=0;i10000;i++){("bbb");}();
这段代码中,只会创建一个builder对象,每次循环都会使用这个builder对象进行拼接,因此提高了拼接效率。
我们前面说过,String类是典型的Immutable不可变类实现,保证了线程安全性,所有对String字符串的修改都会构造出一个新的String对象,由于String的不可变性,不可变对象在拷贝时不需要额外的复制数据。
String在之后提供了intern()方法,intern方法是一个native方法,它底层由C/C++实现,intern方法的目的就是为了把字符串缓存起来,在中却不推荐使用intern方法,因为把方法区放到了永久代(Java堆的一部分),永久代的空间是有限的,除了Fullgc外,其他收集并不会释放永久代的存储空间。将字符串常量池移到了堆内存中,
下面我们来看一段代码,来认识一下intern方法
publicstaticvoidmain(String[]args){Stringa=newString("ab");Stringb=newString("ab");Stringc="ab";Stringd="a";Stringe=newString("b");Stringf=d+e;(()==b);(()==());(()==c);(()==f);}
上述的执行结果是什么呢?我们先把答案贴出来,以防心急的同学想急于看到结果,他们的答案是
falsetruetruefalse
和你预想的一样吗?为什么会这样呢?我们先来看一下intern方法的官方解释
这里你需要知道JVM的内存模型
虚拟机栈:Java虚拟机栈是线程私有的数据区,Java虚拟机栈的生命周期与线程相同,虚拟机栈也是局部变量的存储位置。方法在执行过程中,会在虚拟机栈种创建一个栈帧(stackframe)。
本地方法栈:本地方法栈也是线程私有的数据区,本地方法栈存储的区域主要是Java中使用native关键字修饰的方法所存储的区域
程序计数器:程序计数器也是线程私有的数据区,这部分区域用于存储线程的指令地址,用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都通过程序计数器来完成。
方法区:方法区是各个线程共享的内存区域,它用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆:堆是线程共享的数据区,堆是JVM中最大的一块存储区域,所有的对象实例都会分配在堆上
运行时常量池:运行时常量池又被称为RuntimeConstantPool,这块区域是方法区的一部分,它的名字非常有意思,它并不要求常量一定只有在编译期才能产生,也就是并非编译期间将常量放在常量池中,运行期间也可以将新的常量放入常量池中,String的intern方法就是一个典型的例子。
在及之前的版本中,常量池是分配在方法区中永久代(ParmanentGeneration)内的,而永久代和Java堆是两个完全分开的区域。如果字符串常量池中已经包含一个等于此String对象的字符串,则返回常量池中这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
从开始去永久代,字符串常量池已经被转移至Java堆中,开发人员也对intern方法做了一些修改。因为字符串常量池和new的对象都存于Java堆中,为了优化性能和减少内存开销,当调用intern方法时,如果常量池中已经存在该字符串,则返回池中字符串;否则直接存储堆中的引用,也就是字符串常量池中存储的是指向堆里的对象。
所以我们对上面的结论进行分析
Stringa=newString("ab");Stringb=newString("ab");(()==b);
输出什么?false,为什么呢?画一张图你就明白了(图画的有些问题,栈应该是后入先出,所以b应该在a上面,不过不影响效果)
返回的是常量池中的ab,而b是直接返回的是堆中的ab。地址不一样,肯定输出false
所以第二个
(()==());
也就没问题了吧,它们都返回的是字符串常量池中的ab,地址相同,所以输出true
然后来看第三个
(()==c);
图示如下
a不会变,因为常量池中已经有了ab,所以c不会再创建一个ab字符串,这是编译器做的优化,为了提高效率。
下面来看最后一个
(()==f);
首先来看一下String类在继承树的什么位置、实现了什么接口、父类是谁,这是源码分析的几大重要因素。
String没有继承任何接口,不过实现了三个接口,分别是**Serializable、Comparable、CharSequence**接口
Serializable:这个序列化接口没有任何方法和域,仅用于标识序列化的语意。
Comparable:实现了Comparable的接口可用于内部比较两个对象的大小
CharSequence:字符串序列接口,CharSequence是一个可读的char值序列,提供了length(),charAt(intindex),subSequence(intstart,int)等接口,StringBuilder和StringBuffer也继承了这个接口
重要属性
字符串是什么,字!符!串!你品,你细品。你会发现它就是一连串字符组成的串。
也就是说
Stringstr="abc";//===chardata[]={'a','b','c'};Stringstr=newString(data);
原来这么回事啊!
所以,String中有一个用于存储字符的char数组value[],这个数组存储了每个字符。另外一个就是hash属性,它用于缓存字符串的哈希码。因为String经常被用于比较,比如在HashMap中。如果每次进行比较都重新计算其hashcode的值的话,那无疑是比较麻烦的,而保存一个hashcode的缓存无疑能优化这样的操作。
String可以通过许多途径创建,也可以根据Stringbuffer和StringBuilder进行创建。
毕竟我们本篇文章探讨的不是源码分析的文章,所以涉及到的源码不会很多。
除此之外,String还提供了一些其他方法
charAt:返回指定位置上字符的值
getChars:复制String中的字符到指定的数组
equals:用于判断String对象的值是否相等
indexOf:用于检索字符串
substring:对字符串进行截取
concat:用于字符串拼接,效率高于+
replace:用于字符串替换
match:正则表达式的字符串匹配
contains:是否包含指定字符序列
split:字符串分割
join:字符串拼接
trim:去掉多余空格
toCharArray:把String对象转换为字符数组
valueOf:把对象转换为字符串
StringBuilder类表示一个可变的字符序列,我们知道,StringBuilder是非线程安全的容器,一般适用于单线程场景中的字符串拼接操作,下面我们就来从源码角度看一下StringBuilder
首先我们来看一下StringBuilder的定义
publicfi,CharSequence{}
StringBuilder被final修饰,表示StringBuilder是不可被继承的,StringBuilder类继承于AbstractStringBuilder类。实际上,AbstractStringBuilder类具体实现了可变字符序列的一系列操作,比如:app()、insert()、delete()、replace()、charAt()方法等。
StringBuilder实现了2个接口
Serializable序列化接口,表示对象可以被序列化。
CharSequence字符序列接口,提供了几个对字符序列进行只读访问的方法,例如length()、charAt()、subSequence()、toString()方法等。
StringBuilder使用AbstractStringBuilder类中的两个变量作为元素
char[]value;//存储字符数组intcount;//字符串使用的计数StringBuffer
StringBuffer也是继承于AbstractStringBuilder,使用value和count分别表示存储的字符数组和字符串使用的计数,StringBuffer与StringBuilder最大的区别就是StringBuffer可以在多线程场景下使用,StringBuffer内部有大部分方法都加了synchronized锁。在单线程场景下效率比较低,因为有锁的开销。
我相信这个问题很多同学都没有注意到吧,其实StringBuilder和StringBuffer存在扩容问题,先从StringBuilder开始看起
首先先注意一下StringBuilder的初始容量
publicStringBuilder(){super(16);}
StringBuilder的初始容量是16,当然也可以指定StringBuilder的初始容量。
在调用app拼接字符串,会调用AbstractStringBuilder中的app方法
publicAbstractStringBuilderapp(Stringstr){if(str==null)returnappNull();intlen=();ensureCapacityInternal(count+len);(0,len,value,count);count+=len;returnthis;}
上面代码中有一个ensureCapacityInternal方法,这个就是扩容方法,我们跟进去看一下
privatevoidensureCapacityInternal(intminimumCapacity){//overflow-consciouscodeif(){value=(value,newCapacity(minimumCapacity));}}
这个方法会进行判断,minimumCapacity就是字符长度+要拼接的字符串长度,如果拼接后的字符串要比当前字符长度大的话,会进行数据的复制,真正扩容的方法是在newCapacity中
privateintnewCapacity(intminCapacity){//overflow-consciouscodeintnewCapacity=()+2;if(newCapacity-minCapacity0){newCapacity=minCapacity;}return(newCapacity=0||MAX_ARRAY_SIZE-newCapacity0)?hugeCapacity(minCapacity):newCapacity;}
扩容后的字符串长度会是原字符串长度增加一倍+2,如果扩容后的长度还比拼接后的字符串长度小的话,那就直接扩容到它需要的长度newCapacity=minCapacity,然后再进行数组的拷贝。
本篇文章主要描述了String、StringBuilder和StringBuffer的主要特性,String、StringBuilder和StringBuffer的底层构造是怎样的,以及String常量池的优化、StringBuilder和StringBuffer的扩容特性等。
如果有错误的地方,还请大佬们提出宝贵意见。
版权声明:本站所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请举报,一经查实,本站将立刻删除。