結(jié)合代碼來看
public static void main(String[]args){
/**
*情景一:字符串池
*JAVA虛擬機(JVM)中存在著一個字符串池,其中保存著很多String對象;
*并且可以被共享使用求摇,因此它提高了效率殊者。
*由于String類是final的猖吴,它的值一經(jīng)創(chuàng)建就不可改變挥转。
*字符串池由String類維護,我們可以調(diào)用intern()方法來訪問字符串池绑谣。
*/
String s1="abc";
//↑在字符串池創(chuàng)建了一個對象
String s2="abc";
//↑字符串pool已經(jīng)存在對象“abc”(共享),所以創(chuàng)建0個對象,累計創(chuàng)建一個對象
System.out.println("s1==s2:"+(s1==s2));
//↑true指向同一個對象幌衣,
System.out.println("s1.equals(s2):"+(s1.equals(s2)));
//↑true值相等
//↑------------------------------------------------------over
/**
/*
*情景二:關于new String("")
*/
String s3 = new String("abc");
//↑創(chuàng)建了兩個對象壤玫,一個 "abc" 存放在字符串池中,一個 String 對象存在與堆區(qū)中楚里;
//↑還有一個對象引用s3存放在棧中
String s4 = new String("abc");
//↑字符串池中已經(jīng)存在“abc”對象猎贴,所以只在堆中創(chuàng)建了一個String對象
//↑還有一個對象引用s4存放在棧中
System.out.println("s3==s4:"+(s3==s4));
//↑false s3和s4指向堆區(qū)的不同地址;
System.out.println("s3.equals(s4):"+(s3.equals(s4)));
//↑true s3和s4的值相同
System.out.println("s1==s3:"+(s1==s3));
//↑false存放的地區(qū)不同吝梅,一個棧區(qū)惹骂,一個堆區(qū)
System.out.println("s1.equals(s3):"+(s1.equals(s3)));
//↑true值相同
//↑------------------------------------------------------over
/**
/*情景三:
*由于常量的值在編譯的時候就被確定(優(yōu)化)了。
*在這里右冻,"ab"和"cd"都是常量著拭,因此變量str3的值在編譯時就可以確定。
*這行代碼編譯后的效果等同于:String str1 ="abcd";
*/
String str1 = "ab" + "cd";//1個對象,在編譯時直接創(chuàng)建為"abcd"
String str11="abcd";
System.out.println("str1=str11:"+(str1==str11));
//↑--------true--------同情景一
/**
/*情景四:
*局部變量str2,str3存儲的是存儲兩個拘留字符串對象(intern字符串對象)的地址乳蛾。
*
*第三行代碼原理(str2+str3):
*運行期JVM首先會在堆中創(chuàng)建一個StringBuilder類,
*同時用str2指向的拘留字符串對象完成初始化肃叶,
*然后調(diào)用append方法完成對str3所指向的拘留字符串的合并因惭,
*接著調(diào)用StringBuilder的toString()方法在堆中創(chuàng)建一個String對象,
*最后將剛生成的String對象的堆地址存放在局部變量str3中蹦魔。
*
*而str5存儲的是字符串池中"abcd"所對應的拘留字符串對象的地址。
*str4與str5地址當然不一樣了柱搜。
*
*內(nèi)存中實際上有五個字符串對象:
*三個拘留字符串對象剥险、一個String對象和一個StringBuilder對象。
*/
String str2 = "ab";//1個對象
String str3 = "cd";//1個對象
String str4 = str2+str3;
String str5 = "abcd";
System.out.println("str4=str5:"+(str4==str5));//false
//↑------------------------------------------------------over
/**
/*情景五:
*JAVA編譯器對string+基本類型/常量是當成常量表達式直接求值來優(yōu)化的健爬。
*運行期的兩個string相加么介,會產(chǎn)生新的對象的,存儲在堆(heap)中
*/
Stringstr6="b";
Stringstr7="a"+str6;
Stringstr67="ab";
System.out.println("str7=str67:"+(str7==str67));
//↑str6為變量设拟,在運行期才會被解析久脯。
finalStringstr8="b";
Stringstr9="a"+str8;
Stringstr89="ab";
System.out.println("str9=str89:"+(str9==str89));
//↑str8為常量變量,編譯期會被優(yōu)化
//↑------------------------------------------------------over
}
總結(jié):
1.String類初始化后是不可變的(immutable)
這一說又要說很多帘撰,大家只要知道String的實例一旦生成就不會再改變了摧找,比如說:Stringstr=”kv”+”ill”+”“+”ans”;是有4個字符串常量,首先”kv”和”ill”生成了”kvill”存在內(nèi)存中蹬耘,然后”kvill”又和””生成“kvill“存在內(nèi)存中,最后又和生成了”kvillans”;并把這個字符串的地址賦給了str,就是因為String的”不可變”產(chǎn)生了很多臨時變量惩系,這也就是為什么建議用StringBuffer的原因了,因為StringBuffer是可改變的赃承。
下面是一些String相關的常見問題:
String中的final用法和理解
finalStringBuffera=newStringBuffer(“111”);
finalStringBufferb=newStringBuffer(“222”);
a=b;//此句編譯不通過finalStringBuffera=newStringBuffer(“111”);
a.append(“222”);//編譯通過
可見悴侵,final只對引用的”值”(即內(nèi)存地址)有效拭嫁,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤浇借。至于它所指向的對象的變化怕品,final是不負責的。
2.代碼中的字符串常量在編譯的過程中收集并放在class文件的常量區(qū)中闯估,如”123”吼和、”123”+”456”等,含有變量的表達式不會收錄炫乓,如”123”+a。
3.JVM在加載類的時候侠姑,根據(jù)常量區(qū)中的字符串生成常量池箩做,每個字符序列如”123”會生成一個實例放在常量池里,這個實例是不在堆里的船老,也不會被GC圃酵,這個實例的value屬性從源碼的構(gòu)造函數(shù)看應該是用new創(chuàng)建數(shù)組置入123的,所以按我的理解此時value存放的字符數(shù)組地址是在堆里薪韩,如果有誤的話歡迎大家指正。
4.使用String不一定創(chuàng)建對象
在執(zhí)行到雙引號包含字符串的語句時俘陷,如Stringa=“123”,JVM會先到常量池里查找桨菜,如果有的話返回常量池里的這個實例的引用捉偏,否則的話創(chuàng)建一個新實例并置入常量池里。如果是Stringa=“123”+b(假設b是”456”)霞掺,前半部分”123”還是走常量池的路線讹躯,但是這個+操作符其實是轉(zhuǎn)換成[SringBuffer].Appad()來實現(xiàn)的,所以最終a得到是一個新的實例引用骗灶,而且a的value存放的是一個新申請的字符數(shù)組內(nèi)存空間的地址(存放著”123456”)酷麦,而此時”123456”在常量池中是未必存在的。
要注意:我們在使用諸如Stringstr=“abc”沃饶;的格式定義類時糊肤,總是想當然地認為,創(chuàng)建了String類的對象str馆揉。擔心陷阱!對象可能并沒有被創(chuàng)建舷暮!而可能只是指向一個先前已經(jīng)創(chuàng)建的對象噩茄。只有通過new()方法才能保證每次都創(chuàng)建一個新的對象
5.使用newString,一定創(chuàng)建對象
在執(zhí)行Stringa=newString(“123”)的時候沥割,首先走常量池的路線取到一個實例的引用,然后在堆上創(chuàng)建一個新的String實例帜讲,走以下構(gòu)造函數(shù)給value屬性賦值椒拗,然后把實例引用賦值給a:
publicString(Stringoriginal){
intsize=original.count;
char[]originalValue=original.value;
char[]v;
if(originalValue.length>size){
//ThearrayrepresentingtheStringisbiggerthanthenew
//Stringitself.Perhapsthisconstructorisbeingcalled
//inordertotrimthebaggage,somakeacopyofthearray.
intoff=original.offset;
v=Arrays.copyOfRange(originalValue,off,off+size);
}else{
//ThearrayrepresentingtheStringisthesame
//sizeastheString,sonopointinmakingacopy.
v=originalValue;
}
this.offset=0;
this.count=size;
this.value=v;
}
從中我們可以看到,雖然是新創(chuàng)建了一個String的實例玩郊,但是value是等于常量池中的實例的value枉阵,即是說沒有new一個新的字符數(shù)組來存放”123”预茄。
如果是Stringa=newString(“123”+b)的情況,首先看回第4點拙徽,”123”+b得到一個實例后诗宣,再按上面的構(gòu)造函數(shù)執(zhí)行。
6.String.intern()
String對象的實例調(diào)用intern方法后岛心,可以讓JVM檢查常量池篮灼,如果沒有實例的value屬性對應的字符串序列比如”123”(注意是檢查字符串序列而不是檢查實例本身),就將本實例放入常量池髓堪,如果有當前實例的value屬性對應的字符串序列”123”在常量池中存在娘荡,則返回常量池中”123”對應的實例的引用而不是當前實例的引用,即使當前實例的value也是”123”炮沐。
publicnativeStringintern();
存在于.class文件中的常量池央拖,在運行期被JVM裝載鹉戚,并且可以擴充专控。String的intern()方法就是擴充常量池的一個方法;當一個String實例str調(diào)用intern()方法時赢底,Java查找常量池中是否有相同Unicode的字符串常量柏蘑,如果有,則返回其的引用咳焚,如果沒有革半,則在常量池中增加一個Unicode等于str的字符串并返回它的引用;看示例就清楚了
publicstaticvoidmain(String[]args){
Strings0="kvill";
Strings1=newString("kvill");
Strings2=newString("kvill");
System.out.println(s0==s1);//false
System.out.println("**********");
s1.intern();//雖然執(zhí)行了s1.intern(),但它的返回值沒有賦給s1
s2=s2.intern();//把常量池中"kvill"的引用賦給s2
System.out.println(s0==s1);//flase
System.out.println(s0==s1.intern());//true//說明s1.intern()返回的是常量池中"kvill"的引用
System.out.println(s0==s2);//true
}
最后我再破除一個錯誤的理解:有人說延刘,“使用String.intern()方法則可以將一個String類的保存到一個全局String表中六敬,如果具有相同值的Unicode字符串已經(jīng)在這個表中,那么該方法返回表中已有字符串的地址外构,如果在表中沒有相同值的字符串典勇,則將自己的地址注冊到表中”如果我把他說的這個全局的String表理解為常量池的話,他的最后一句話割笙,”如果在表中沒有相同值的字符串伤溉,則將自己的地址注冊到表中”是錯的:
publicstaticvoidmain(String[]args){
Strings1=newString("kvill");
Strings2=s1.intern();
System.out.println(s1==s1.intern());//false
System.out.println(s1+""+s2);//kvillkvill
System.out.println(s2==s1.intern());//true
}
在這個類中我們沒有聲名一個”kvill”常量,所以常量池中一開始是沒有”kvill”的乱顾,當我們調(diào)用s1.intern()后就在常量池中新添加了一個”kvill”常量走净,原來的不在常量池中的”kvill”仍然存在孤里,也就不是“將自己的地址注冊到常量池中”了橘洞。
s1==s1.intern()為false說明原來的”kvill”仍然存在;s2現(xiàn)在為常量池中”kvill”的地址虏等,所以有s2==s1.intern()為true适肠。
StringBuffer與StringBuilder的區(qū)別,它們的應用場景是什么敦跌?
jdk的實現(xiàn)中StringBuffer與StringBuilder都繼承自AbstractStringBuilder沸毁,對于多線程的安全與非安全看到StringBuffer中方法前面的一堆synchronized就大概了解了傻寂。
這里隨便講講AbstractStringBuilder的實現(xiàn)原理:我們知道使用StringBuffer等無非就是為了提高java中字符串連接的效率,因為直接使用+進行字符串連接的話搂誉,jvm會創(chuàng)建多個String對象静檬,因此造成一定的開銷。AbstractStringBuilder中采用一個char數(shù)組來保存需要append的字符串侮腹,char數(shù)組有一個初始大小稻励,當append的字符串長度超過當前char數(shù)組容量時,則對char數(shù)組進行動態(tài)擴展加矛,也即重新申請一段更大的內(nèi)存空間煤篙,然后將當前char數(shù)組拷貝到新的位置,因為重新分配內(nèi)存并拷貝的開銷比較大苛茂,所以每次重新申請內(nèi)存空間都是采用申請大于當前需要的內(nèi)存空間的方式,這里是2倍
【
StringBuffer始于JDK1.0
StringBuilder始于JDK1.5
從JDK1.5開始妓羊,帶有字符串變量的連接操作(+)侍瑟,JVM內(nèi)部采用的是
StringBuilder來實現(xiàn)的,而之前這個操作是采用StringBuffer實現(xiàn)的涨颜。
】
我們通過一個簡單的程序來看其執(zhí)行的流程:
publicclassBuffer{
publicstaticvoidmain(String[]args){
Strings1="aaaaa";
Strings2="bbbbb";
Stringr=null;
inti=3694;
r=s1+i+s2;
for(intj=0;i<10;j++){
r+="23124";
}
}
}
使用命令javap-cBuffer查看其字節(jié)碼實現(xiàn):
將清單1和清單2對應起來看庭瑰,清單2的字節(jié)碼中l(wèi)dc指令即從常量池中加載“aaaaa”字符串到棧頂,istore_1將“aaaaa”存到變量1中督暂,后面的一樣穷吮,sipush是將一個短整型常量值(-32768~32767)推送至棧頂,這里是常量“3694”八回。
讓我們直接看到13,1317是new了一個StringBuffer對象并調(diào)用其初始化方法驾诈,2021則是先通過aload_1將變量1壓到棧頂,前面說過變量1放的就是字符串常量“aaaaa”管引,接著通過指令invokevirtual調(diào)用StringBuffer的append方法將“aaaaa”拼接起來闯两,后續(xù)的24~30同理。最后在33調(diào)用StringBuffer的toString函數(shù)獲得String結(jié)果并通過astore存到變量3中噩翠。
看到這里可能有人會說邦投,“既然JVM內(nèi)部采用了StringBuffer來連接字符串了,那么我們自己就不用用StringBuffer屯援,直接用”+“就行了吧!“狞洋。是么?當然不是了庐橙。俗話說”存在既有它的理由”借嗽,讓我們繼續(xù)看后面的循環(huán)對應的字節(jié)碼。
3742都是進入for循環(huán)前的一些準備工作惨寿,37,38是將j置為1裂垦。44這里通過if_icmpge將j與10進行比較,如果j大于10則直接跳轉(zhuǎn)到73缸废,也即return語句退出函數(shù)企量;否則進入循環(huán)亡电,也即4766的字節(jié)碼。這里我們只需看47到51就知道為什么我們要在代碼中自己使用StringBuffer來處理字符串的連接了份乒,因為每次執(zhí)行“+”操作時jvm都要new一個StringBuffer對象來處理字符串的連接或辖,這在涉及很多的字符串連接操作時開銷會很大。