在做java開(kāi)發(fā)中,我們一定沒(méi)少使用String這個(gè)東西,對(duì)它可謂是非常熟悉了,但是我今天要來(lái)仔細(xì)的學(xué)習(xí)學(xué)習(xí)String;
初看String
首先學(xué)習(xí)一個(gè)類的最好方式肯定是看源碼,我對(duì)源碼整理一下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
........
}
大概就是這個(gè)樣子,我們從中可以得到很多信息:
首先,String是被final修飾的,因此便知道為什么不能被繼承了,且成員方法都是默認(rèn)final類型;
其次,String的底層實(shí)現(xiàn)是使用char數(shù)組來(lái)保存字符串;
因?yàn)镾tring是被final修飾的,因此其實(shí)不可被修改的,那么我們平時(shí)使用的concat、replace、sub等是怎么回事尼?跟進(jìn)去看看就可以發(fā)現(xiàn):
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
public String concat(String str) {
...
return new String(0, count + otherLen, buf);
}
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = count;
int i = -1;
char[] val = value; /* avoid getfield opcode */
int off = offset; /* avoid getfield opcode */
while (++i < len) {
if (val[off + i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0 ; j < i ; j++) {
buf[j] = val[off+j];
}
while (i < len) {
char c = val[off + i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(0, len, buf);
}
}
return this;
}
無(wú)論是sub、concat、replace操作都不是在原有的字符串上進(jìn)行的,而是重新生成了一個(gè)新的字符串對(duì)象。一句話解釋就是,最原始的字符串始終沒(méi)有被改變。
我們可以得出一個(gè)結(jié)論:
String對(duì)象一旦被創(chuàng)建就固定不變了,對(duì)String對(duì)象的任何操作都不會(huì)影響到原對(duì)象,所改變都是生成了新的對(duì)象
常量池
String和其他類一樣,產(chǎn)生對(duì)象就得為其分配空間,但是String的使用太常見(jiàn)了,JVM為了提高性能,就在字符串實(shí)例化的時(shí)候做了一些優(yōu)化:
每當(dāng)我們創(chuàng)建字符串常量時(shí),JVM會(huì)首先檢查字符串常量池,如果該字符串已經(jīng)存在常量池中,那么就直接返回常量池中的實(shí)例引用。如果字符串不存在常量池中,就會(huì)實(shí)例化該字符串并且將其放到常量池中。由于String字符串的不可變性我們可以十分肯定常量池中一定不存在兩個(gè)相同的字符串.
String b = "chenssy";
String b = "chenssy";
String c = new String("chenssy");
a、b和字面上的chenssy都是指向JVM字符串常量池中的"chenssy"對(duì)象,他們指向同一個(gè)對(duì)象;new關(guān)鍵字一定會(huì)產(chǎn)生一個(gè)對(duì)象chenssy(注意這個(gè)chenssy和上面的chenssy不同),同時(shí)這個(gè)對(duì)象是存儲(chǔ)在堆中。所以上面應(yīng)該產(chǎn)生了兩個(gè)對(duì)象:保存在棧中的和保存堆中的。但是在Java中根本就不存在兩個(gè)完全一模一樣的字符串對(duì)象。故堆中的chenssy應(yīng)該是引用字符串常量池中chenssy。
1.用new String() 創(chuàng)建的字符串不是常量,不能在編譯期就確定,所以new String() 創(chuàng)建的字符串不放入常量池中,它們有自己的地址空間.
2.JVM對(duì)String str="abc"對(duì)象放在常量池中是在編譯時(shí)做的,而String str1=str+str是在運(yùn)行時(shí)刻才能知道的,new對(duì)象也是在運(yùn)行時(shí)才做的.
3.字符串的"+"連接中,如果有字符串引用存在,而引用的值在程序編譯期是無(wú)法確定的,例如s2="a"+s1;s1="b";即"a" + s1無(wú)法被編譯器優(yōu)化,只有在程序運(yùn)行期來(lái)動(dòng)態(tài)分配并將連接后的新地址賦給s2;字面量"+"拼接是在編譯期間進(jìn)行的,拼接后的字符串存放在字符串池中;而字符串引用的"+"拼接運(yùn)算實(shí)在運(yùn)行時(shí)進(jìn)行的,新創(chuàng)建的字符串存放在堆中。
字符串重要提示
- String使用private final char value[]來(lái)實(shí)現(xiàn)字符串的存儲(chǔ),也就是說(shuō)String對(duì)象創(chuàng)建之后,就不能再修改此對(duì)象中存儲(chǔ)的字符串內(nèi)容,就是因?yàn)槿绱耍耪f(shuō)String類型是不可變的(immutable),而且String的編輯功能是通過(guò)創(chuàng)建一個(gè)新的對(duì)象來(lái)實(shí)現(xiàn)的,而不是對(duì)原有對(duì)象進(jìn)行修改,例如replace。
- A a;這個(gè)語(yǔ)句聲明一個(gè)類A的引用變量a[我們常常稱之為句柄],而對(duì)象一般通過(guò)new創(chuàng)建,所以aa僅僅是一個(gè)引用變量,它不是對(duì)象。
- 創(chuàng)建字符串的方式
(1)使用""引號(hào)創(chuàng)建字符串;
(2)使用new關(guān)鍵字創(chuàng)建字符串。
(3)單獨(dú)使用""引號(hào)創(chuàng)建的字符串都是常量,編譯期就已經(jīng)確定存儲(chǔ)到String Pool中;
(4)使用new String("")創(chuàng)建的對(duì)象會(huì)存儲(chǔ)到heap中,是運(yùn)行期新創(chuàng)建的;new創(chuàng)建字符串時(shí)首先查看池中是否有相同值的字符串,如果有,則拷貝一份到堆中,然后返回堆中的地址;如果池中沒(méi)有,則在堆中創(chuàng)建一份,然后返回堆中的地址(注意,此時(shí)不需要從堆中復(fù)制到池中,否則,將使得堆中的字符串永遠(yuǎn)是池中的子集,導(dǎo)致浪費(fèi)池的空間)!
(5)使用只包含常量的字符串連接符如"aa" + "aa"創(chuàng)建的也是常量,編譯期就能確定,已經(jīng)確定存儲(chǔ)到String Pool中;
(6)使用包含變量的字符串連接符如"aa" + s1創(chuàng)建的對(duì)象是運(yùn)行期才創(chuàng)建的,存儲(chǔ)在heap中; - 在執(zhí)行到雙引號(hào)包含字符串的語(yǔ)句時(shí),如String a = "123",JVM會(huì)先到常量池里查找,如果有的話返回常量池里的這個(gè)實(shí)例的引用,否則的話創(chuàng)建一個(gè)新實(shí)例并置入常量池里。所以,當(dāng)我們?cè)谑褂弥T如String str = "abc";的格式定義對(duì)象時(shí),總是想當(dāng)然地認(rèn)為,創(chuàng)建了String類的對(duì)象str。對(duì)象可能并沒(méi)有被創(chuàng)建!而可能只是指向一個(gè)先前已經(jīng)創(chuàng)建的對(duì)象。只有通過(guò)new()方法才能保證每次都創(chuàng)建一個(gè)新的對(duì)象。
- 在執(zhí)行String a = new String("123")的時(shí)候,首先走常量池的路線取到一個(gè)實(shí)例的引用,然后在堆上創(chuàng)建一個(gè)新的String實(shí)例,通過(guò)構(gòu)造函數(shù)給value屬性賦值,然后把實(shí)例引用賦值給a:雖然是新創(chuàng)建了一個(gè)String的實(shí)例,但是value是等于常量池中的實(shí)例的value,即是說(shuō)沒(méi)有new一個(gè)新的字符數(shù)組來(lái)存放"123"。
- intern方法使用:一個(gè)初始為空的字符串池,它由類String獨(dú)自維護(hù)。當(dāng)調(diào)用 intern方法時(shí),如果池已經(jīng)包含一個(gè)等于此String對(duì)象的字符串(用equals(oject)方法確定),則返回池中的字符串。否則,將此String對(duì)象添加到池中,并返回此String對(duì)象的引用。它遵循以下規(guī)則:對(duì)于任意兩個(gè)字符串 s 和 t,當(dāng)且僅當(dāng) s.equals(t) 為 true 時(shí),s.intern() == t.intern() 才為 true
- 關(guān)于equals和==
(1)對(duì)于==,如果作用于基本數(shù)據(jù)類型的變量(byte,short,char,int,long,float,double,boolean ),則直接比較其存儲(chǔ)的"值"是否相等;如果作用于引用類型的變量(String),則比較的是所指向的對(duì)象的地址(即是否指向同一個(gè)對(duì)象)。
(2)equals方法是基類Object中的方法,因此對(duì)于所有的繼承于Object的類都會(huì)有該方法。在Object類中,equals方法是用來(lái)比較兩個(gè)對(duì)象的引用是否相等,即是否指向同一個(gè)對(duì)象。
(3)對(duì)于equals方法,注意:equals方法不能作用于基本數(shù)據(jù)類型的變量。如果沒(méi)有對(duì)equals方法進(jìn)行重寫,則比較的是引用類型的變量所指向的對(duì)象的地址;而String類對(duì)equals方法進(jìn)行了重寫,用來(lái)比較指向的字符串對(duì)象所存儲(chǔ)的字符串是否相等。其他的一些類諸如Double,Date,Integer等,都對(duì)equals方法進(jìn)行了重寫用來(lái)比較指向的對(duì)象所存儲(chǔ)的內(nèi)容是否相等。 - String中的"+"
(1).String中使用 + 字符串連接符進(jìn)行字符串連接時(shí),連接操作最開(kāi)始時(shí)如果都是字符串常量,編譯后將盡可能多的直接將字符串常量連接起來(lái),形成新的字符串常量參與后續(xù)連接(通過(guò)反編譯工具jd-gui也可以方便的直接看出);
(2).接下來(lái)的字符串連接是從左向右依次進(jìn)行,對(duì)于不同的字符串,首先以最左邊的字符串為參數(shù)創(chuàng)建StringBuilder對(duì)象,然后依次對(duì)右邊進(jìn)行append操作,最后將StringBuilder對(duì)象通過(guò)toString()方法轉(zhuǎn)換成String對(duì)象(注意:中間的多個(gè)字符串常量不會(huì)自動(dòng)拼接)。
也就是說(shuō)String c = "xx" + "yy " + a + "zz" + "mm" + b; 實(shí)質(zhì)上的實(shí)現(xiàn)過(guò)程是: String c = new StringBuilder("xxyy ").append(a).append("zz").append("mm").append(b).toString();
由此得出結(jié)論:當(dāng)使用+進(jìn)行多個(gè)字符串連接時(shí),實(shí)際上是產(chǎn)生了一個(gè)StringBuilder對(duì)象和一個(gè)String對(duì)象。 - String、StringBuffer、StringBuilder的區(qū)別
(1)可變與不可變:String是不可變字符串對(duì)象,StringBuilder和StringBuffer是可變字符串對(duì)象(其內(nèi)部的字符數(shù)組長(zhǎng)度可變)。
(2)是否多線程安全:String中的對(duì)象是不可變的,也就可以理解為常量,顯然線程安全。StringBuffer 與 StringBuilder 中的方法和功能完全是等價(jià)的,只是StringBuffer 中的方法大都采用了synchronized 關(guān)鍵字進(jìn)行修飾,因此是線程安全的,而 StringBuilder 沒(méi)有這個(gè)修飾,可以被認(rèn)為是非線程安全的。
(3)String、StringBuilder、StringBuffer三者的執(zhí)行效率:
StringBuilder > StringBuffer > String 當(dāng)然這個(gè)是相對(duì)的,不一定在所有情況下都是這樣。比如String str = "hello"+ "world"的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高。因此,這三個(gè)類是各有利弊,應(yīng)當(dāng)根據(jù)不同的情況來(lái)進(jìn)行選擇使用:
當(dāng)字符串相加操作或者改動(dòng)較少的情況下,建議使用 String str="hello"這種形式;
當(dāng)字符串相加操作較多的情況下,建議使用StringBuilder,如果采用了多線程,則使用StringBuffer。 - 關(guān)于String str = new String("abc")創(chuàng)建了多少個(gè)對(duì)象?
new只調(diào)用了一次,也就是說(shuō)只創(chuàng)建了一個(gè)對(duì)象。而這道題目讓人混淆的地方就是這里,這段代碼在運(yùn)行期間確實(shí)只創(chuàng)建了一個(gè)對(duì)象,即在堆上創(chuàng)建了"abc"對(duì)象。而為什么大家都在說(shuō)是2個(gè)對(duì)象呢,這里面要澄清一個(gè)概念,該段代碼執(zhí)行過(guò)程和類的加載過(guò)程是有區(qū)別的。在類加載的過(guò)程中,確實(shí)在運(yùn)行時(shí)常量池中創(chuàng)建了一個(gè)"abc"對(duì)象,而在代碼執(zhí)行過(guò)程中確實(shí)只創(chuàng)建了一個(gè)String對(duì)象。
因此,這個(gè)問(wèn)題如果換成 String str = new String("abc")涉及到幾個(gè)String對(duì)象?合理的解釋是2個(gè)。
個(gè)人覺(jué)得在面試的時(shí)候如果遇到這個(gè)問(wèn)題,可以向面試官詢問(wèn)清楚”是這段代碼執(zhí)行過(guò)程中創(chuàng)建了多少個(gè)對(duì)象還是涉及到多少個(gè)對(duì)象“
String.split()用法小結(jié)
在java.lang包中有String.split()方法,返回是一個(gè)數(shù)組;
1、如果用“.”作為分隔的話,必須是如下寫法,String.split("\."),這樣才能正確的分隔開(kāi),不能用String.split(".");
2、如果用“|”作為分隔的話,必須是如下寫法,String.split("\|"),這樣才能正確的分隔開(kāi),不能用String.split("|");“.”和“|”都是轉(zhuǎn)義字符,必須得加"\";
3、如果在一個(gè)字符串中有多個(gè)分隔符,可以用“|”作為連字符,比如,“acount=? and uu =? or n=?”,把三個(gè)都分隔出來(lái),可以用String.split("and|or");使用String.split方法分隔字符串時(shí),分隔符如果用到一些特殊字符,可能會(huì)得不到我們預(yù)期的結(jié)果。
4、用豎 * 分隔字符串運(yùn)行將拋出java.util.regex.PatternSyntaxException異常,用加號(hào) + 也是如此。
5、顯然, + * 不是有效的模式匹配規(guī)則表達(dá)式,用"\*" "\+"轉(zhuǎn)義后即可得到正確的結(jié)果。"|" 分隔串時(shí)雖然能夠執(zhí)行,但是卻不是預(yù)期的目的,"\|"轉(zhuǎn)義后即可得到正確的結(jié)果。還有如果想在串中使用""字符,則也需要轉(zhuǎn)義.首先要表達(dá)"aaaa\bbbb"這個(gè)串就應(yīng)該用"aaaa\bbbb",如果要分隔就應(yīng)該這樣才能得到正確結(jié)果。
String.trim()
String.Trim()方法會(huì)去除字符串兩端,不僅僅是空格字符,它總共能去除25種字符: ('/t', '/n', '/v', '/f', '/r', ' ', '/x0085', '/x00a0', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '?', '/u2028', '/u2029', ' ', '?')
如果你想保留其中的一個(gè)或多個(gè)(例如/t制表符,/n換行符,/r回車符等),請(qǐng)慎用Trim方法。
請(qǐng)注意,Trim刪除的過(guò)程為從外到內(nèi),直到碰到一個(gè)非空白的字符為止,所以不管前后有多少個(gè)連續(xù)的空白字符都會(huì)被刪除掉
空格 != 空白字符,刪除空格請(qǐng)使用: Trim(‘ ‘);