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