Java十八篇:常用類 String

圣誕節(jié)快樂

概念
String 類繼承自 Object 超類,實(shí)現(xiàn)的接口有:Serializable、CharSequence、Comparable接口,具體如下圖:

字符串
什么是字符串?
如果直接按照字面意思來理解就是多個字符連接起來組合成的字符序列。為了更好的理解以上的理論,我們先來解釋下字符序列,字符序列:把多個字符按照一定的順序排列起來;而字符序列就是作為字符串的內(nèi)容而存在的。所以可以把字符串理解為:把多個字符按照一定的順序排列起來而構(gòu)成的排列組合。
如果還是不好理解,沒有關(guān)系,我還有法寶。我們可以用烤串來比喻說明,可以把字符串看作是烤串,烤串上的每一塊肉都相當(dāng)于是一個字符。把一塊塊肉按照肥瘦相間的順序排列并串起來便成了我們吃的烤串,同理,把多個字符按照一定的順序“串”起來就構(gòu)成了字符串。
字符串的分類,字符串分為可變的字符串和不可變的字符串兩種;這里的不可變與可變指的是字符串的對象還是不是同一個,會不會因?yàn)樽址畬ο髢?nèi)容的改變而創(chuàng)建新的對象。
String類型是引用類型,可以通過 String s1 = "abc";直接賦值進(jìn)行實(shí)例化,也可以通過new 關(guān)鍵字實(shí)例化,它也有自己的構(gòu)造函數(shù)。
字符串:就是由多個字符組成的一串?dāng)?shù)據(jù)。也可以看成是一個字符數(shù)組。
通過API,我們可以知道
字符串字面值“abc”也可以看成一個字符串對象。
字符串是常量,一旦被賦值,就不能改變。
閱讀String類定義的源碼,你會發(fā)現(xiàn):
public final class String
? ?implements java.io.Serializable, Comparable<String>, CharSequence {
? ?/** The value is used for character storage. */
? ?private final char value[];
? ?/** Cache the hash code for the string */
? ?private int hash; // Default to 0
? ?...
}
1)String類被final關(guān)鍵字修飾,意味著String類不能被繼承,并且它的成員方法都默認(rèn)為final方法;字符串一旦創(chuàng)建就不能再修改。
2)String類實(shí)現(xiàn)了Serializable、CharSequence、 Comparable接口。
3)String實(shí)例的值是通過字符數(shù)組實(shí)現(xiàn)字符串存儲的。
String 類
String類表示不可變的字符串,當(dāng)前String類對象創(chuàng)建完畢之后,該對象的內(nèi)容(字符序列)是不變的,因?yàn)閮?nèi)容一旦改變就會創(chuàng)建一個一個新的對象。
String對象的創(chuàng)建:
方式一:通過字面量賦值創(chuàng)建,String s1 = “l(fā)aofu”; 需要注意這里是雙引號:“”,區(qū)別與字符char類型的單引號:‘’;
方式二:通過構(gòu)造器創(chuàng)建, String s2 = new String(“l(fā)aofu”);
那問題來了,兩種方式的對象在JVM中又是如何分布的呢? 分別有什么區(qū)別呢?
方式一和方式二在JVM中又是如何分布?

上圖中的常量池:用于存儲常量的地方內(nèi)存區(qū)域,位于方法區(qū)中。常量池又分為編譯常量池和運(yùn)行常量池兩種:
編譯常量池:當(dāng)把字節(jié)碼加載斤JVM的時候,其中存儲的是字節(jié)碼的相關(guān)信息(如:行號等)。運(yùn)行常量池:其中存儲的是代碼中的常量數(shù)據(jù)。
方式一和方式二有何不同?
方式一:String s1 = “l(fā)aofu”; 有可能只創(chuàng)建一個String對象,也有可能創(chuàng)建不創(chuàng)建String對象;如果在常量池中已經(jīng)存在”laofu”,那么對象s1會直接引用,不會創(chuàng)建新的String對象;否則,會先在常量池先創(chuàng)建常量”laofu”的內(nèi)存空間,然后再引用。
方式二:String s2 = new String(“l(fā)aofu”); 最多會創(chuàng)建兩個String對象,最少創(chuàng)建一個String對象??墒褂胣ew關(guān)鍵字創(chuàng)建對象是會在堆空間創(chuàng)建內(nèi)存區(qū)域,這是第一個對象;然后對象中的字符串字面量可能會創(chuàng)建第二個對象,而第二個對象如方式一中所描述的那樣,是有可能會不被創(chuàng)建的,所以至少創(chuàng)建一個String個對象。
字符串的本質(zhì),字符串在底層其實(shí)就是char[],char表示一個字符,比如:
String str = "laofu"; 等價于 char[] cs = new char[]{'l','a','o','f','u'};

String對象的空值:
1、對象引用為空,即:String s1 = null; 此時s1沒有初始化,也在JVM中沒有分配內(nèi)存空間。
2、對象內(nèi)容為空字符串, 比如:String s2 = “”; 此時對象s2已經(jīng)初始化,值為“”,JVM已經(jīng)為其分配內(nèi)存空間。字符串的比較:使用“==”和“equals”會有不同效果,詳情在之前的文章中分享過:Java面向?qū)ο笾?、對象、static成員#對象比較操作
使用”==”號:用于比較對象引用的內(nèi)存地址是否相同。使用equals方法:在Object類中和”==”號相同,但在自定義類中,建議覆蓋equals方法去實(shí)現(xiàn)比較自己內(nèi)容的細(xì)節(jié);由于String類覆蓋已經(jīng)覆蓋了equals方法,所以其比較的是字符內(nèi)容。

所以可以這樣來判斷字符串非空:
對象引用不能為空:s1 != null;,字符內(nèi)容不能為空字符串(“”):“”.equals(s1);如果上述兩個條件都滿足,說明字符串確實(shí)為空!
**字符串拼接:**Java中的字符串可以通過是“+”實(shí)現(xiàn)拼接,那么代碼中字符串拼接在JVM中又是如何處理的呢?我們通過一個例子說明:通過比較拼接字符串代碼編譯前后的代碼來查看JVM對字符串拼接的處理。

通過上述例子不難發(fā)現(xiàn),JVM會對字符串拼接做一些優(yōu)化操作,如果字符串字面量之間的拼接,無論有多少個字符串,JVM都會一樣的處理;如果是對象之間拼接,或者是對象和字面量之間的拼接,亦或是方法執(zhí)行結(jié)果參與拼接,String內(nèi)部會使用StringBuilder先來獲取對象的值,然后使用append方法來執(zhí)行拼接。由此可以總結(jié)得出:
1、使用字符串字面量創(chuàng)建的字符串,也就是單獨(dú)使用""引號創(chuàng)建的字符串都是直接量,在編譯期就會將其存儲到常量池中;
2、使用new String("")創(chuàng)建的對象會存儲到堆內(nèi)存中,在運(yùn)行期才創(chuàng)建;
3、使用只包含直接量的字符串連接符如"aa" + "bb"創(chuàng)建的也是直接量,這樣的字符串在編譯期就能確定,所以也會存儲到常量池中;
4、使用包含String直接量的字符串表達(dá)式(如"aa" + s1)創(chuàng)建的對象是運(yùn)行期才創(chuàng)建的,對象存儲在堆中,因?yàn)槠涞讓邮莿?chuàng)新了StringBuilder對象來實(shí)現(xiàn)拼接的;
5、無論是使用變量,還是調(diào)用方法來連接字符串,都只能在運(yùn)行期才能確定變量的值和方法的返回值,不存在編譯優(yōu)化操作。
String字符串的常用方法。
String類位于java.lang包中,java會默認(rèn)的導(dǎo)入java.lang包,所以我們使用java.lang包的類時,不需要顯示的import類,String類具有豐富的方法,其中比較常用的有:
計(jì)算字符串的長度
連接字符串
比較字符串
提取字符串
替換字符串

案例:
**1、計(jì)算字符串的長度比較簡單。**代碼如下:
String email = "abc@tom.com ?";
System.out.println(email.length());
System.out.println(email.trim().length());
String abc="你好";
System.out.println(abc.length());
第一個輸出是:13。
第二個輸出是:11。
第三個輸出是:2。
因?yàn)榭崭褚菜闶亲址拈L度,如果你不想計(jì)算前后的空格,也也可以使用trim()方法把前后的空格去了。在計(jì)算長度。第三個輸出為2,可見java對中文的處理和英文也是一樣計(jì)算的。并不存在一個中文等于兩個英文符號的長度。
2、連接字符串。
連接字符串有兩種方法:方法1:使用“+”。方法2:使用String類的concat()方法。代碼如下:
System.out.println(email+"abc");
System.out.println(email.concat("abc"));
上面兩個代碼的輸出都是一樣的:
abc@tom.com abc
abc@tom.com abc
3、比較字符串
字符串的比較方法有很多,如比較兩個字符串是否相等,比較后返回值是個布爾值。
String email2="cc@tom.com";
if(email.equals(email2)){
? ? ? ? ? ?System.out.println("相等");
}else{
? ? ? ? ? ?System.out.println("不相等");
}
結(jié)果是不相等。
字符串的比較是提取每一個字符逐個比較的

而且這種比較是區(qū)分大小寫的。所以
String str1 = "java";
? ? ? ?String str2 = "Java";
? ? ? ?System.out.println(str1.equals(str2));
返回為false,如果你想不區(qū)分大小寫的比較,那么你可以使用不區(qū)分大小寫的比較方法或者把字符轉(zhuǎn)為為大寫(也就可以小寫)后再比較。
System.out.println(str1.equalsIgnoreCase(str2));
System.out.println(str1.toLowerCase().equals(str2.toLowerCase()));
System.out.println(str1.toUpperCase().equals(str2.toUpperCase()));
我們還可以判斷字符串是否已某個字符串結(jié)尾或者開頭
System.out.println(email.startsWith("abc"));
System.out.println(email.endsWith("com"));
4、提取字符串
要提取字符串,我們首先得學(xué)會如何查詢字符串。

注意下標(biāo)也是從0開始的。
System.out.println(email.indexOf("a"));//從零開始,所以這里是0
System.out.println(email.indexOf("c"));//這個是2,從左到右找,返回第一個找到的。
System.out.println(email.indexOf("zz"));//找不到返回是-1
System.out.println(email.lastIndexOf("c"));//這個是8,從右到左找,返回第一個找到的。
下面是截取字符串
System.out.println(email.substring(3));//從第4個(包括第四個)開始截取到最后
System.out.println(email.substring(3, 5));
indexOf方法和substring方法經(jīng)常一起使用,比如我們要截取@之后的所有字符串,我們可以結(jié)合這個兩個。
System.out.println(email.substring(email.indexOf("@")+1));
5、替換字符串
例如我們可以把所有的字符串c換成字符串z
System.out.println(email.replace("c", "z"));
通過源碼來理解string類
一、String類
想要了解一個類,最好的辦法就是看這個類的實(shí)現(xiàn)源代碼,來看一下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;
? ?........
}
從上面可以看出幾點(diǎn):
1)String類是final類,也即意味著String類不能被繼承,并且它的成員方法都默認(rèn)為final方法。在Java中,被final修飾的類是不允許被繼承的,并且該類中的成員方法都默認(rèn)為final方法。
2)上面列舉出了String類中所有的成員屬性,從上面可以看出String類其實(shí)是通過char數(shù)組來保存字符串的。
下面再繼續(xù)看String類的一些方法實(shí)現(xiàn):
public String substring(int beginIndex, int endIndex) {
? ?if (beginIndex < 0) {
? ? ? ?thrownew StringIndexOutOfBoundsException(beginIndex);
? ?}
? ?if (endIndex > count) {
? ? ? ?thrownew StringIndexOutOfBoundsException(endIndex);
? ?}
? ?if (beginIndex > endIndex) {
? ? ? ?thrownew StringIndexOutOfBoundsException(endIndex - beginIndex);
? ?}
? ?return ((beginIndex == 0) && (endIndex == count)) ? this :
? ? ? ?newString(offset + beginIndex, endIndex - beginIndex, value);
}
public String concat(String str) {
? ?int otherLen = str.length();
? ?if (otherLen == 0) {
? ? ? ?returnthis;
? ?}
? ?char buf[] = new char[count + otherLen];
? ?getChars(0, count, buf, 0);
? ?str.getChars(0, otherLen, buf, count);
? ?returnnewString(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++;
? ? ? ?}
? ? ? ?returnnewString(0, len, buf);
? ? ? ?}
? ?}
? ?returnthis;
}
從上面的三個方法可以看出,無論是sub操、concat還是replace操作都不是在原有的字符串上進(jìn)行的,而是重新生成了一個新的字符串對象。也就是說進(jìn)行這些操作后,最原始的字符串并沒有被改變。
在這里要永遠(yuǎn)記住一點(diǎn):“String對象一旦被創(chuàng)建就是固定不變的了,對String對象的任何改變都不影響到原對象,相關(guān)的任何change操作都會生成新的對象”。
二、字符串常量池
我們知道字符串的分配和其他對象分配一樣,是需要消耗高昂的時間和空間的,而且字符串我們使用的非常多。JVM為了提高性能和減少內(nèi)存的開銷,在實(shí)例化字符串的時候進(jìn)行了一些優(yōu)化:使用字符串常量池。每當(dāng)我們創(chuàng)建字符串常量時,JVM會首先檢查字符串常量池,如果該字符串已經(jīng)存在常量池中,那么就直接返回常量池中的實(shí)例引用。如果字符串不存在常量池中,就會實(shí)例化該字符串并且將其放到常量池中。由于String字符串的不可變性我們可以十分肯定常量池中一定不存在兩個相同的字符串(這點(diǎn)對理解上面至關(guān)重要)。
Java中的常量池,實(shí)際上分為兩種形態(tài):靜態(tài)常量池和運(yùn)行時常量池。所謂靜態(tài)常量池,即*.class文件中的常量池,class文件中的常量池不僅僅包含字符串(數(shù)字)字面量,還包含類、方法的信息,占用class文件絕大部分空間。而運(yùn)行時常量池,則是jvm虛擬機(jī)在完成類裝載操作后,將class文件中的常量池載入到內(nèi)存中,并保存在方法區(qū)中,我們常說的常量池,就是指方法區(qū)中的運(yùn)行時常量池。
來看下面的程序:
String a = "chenssy";
String b = "chenssy";
a、b和字面上的chenssy都是指向JVM字符串常量池中的"chenssy"對象,他們指向同一個對象。
String c = newString("chenssy");
new關(guān)鍵字一定會產(chǎn)生一個對象chenssy(注意這個chenssy和上面的chenssy不同),同時這個對象是存儲在堆中。所以上面應(yīng)該產(chǎn)生了兩個對象:保存在棧中的c和保存堆中chenssy。但是在Java中根本就不存在兩個完全一模一樣的字符串對象。故堆中的chenssy應(yīng)該是引用字符串常量池中chenssy。所以c、chenssy、池chenssy的關(guān)系應(yīng)該是:c--->chenssy--->池chenssy。整個關(guān)系如下:

通過上面的圖我們可以非常清晰的認(rèn)識他們之間的關(guān)系。所以我們修改內(nèi)存中的值,他變化的是所有。
一道面試題:
public class StringDemo{
?private static final String MESSAGE="taobao";
?public staticvoid main(String [] args) {
? ?String a ="tao"+"bao";
? ?String b="tao";
? ?String c="bao";
? ?System.out.println(a==MESSAGE);
? ?System.out.println((b+c)==MESSAGE);
?}
}
對于這道題,考察的是對String類型的認(rèn)識以及編譯器優(yōu)化。Java中String不是基本類型,但是有些時候和基本類型差不多,如String b = “tao” ; 可以對變量直接賦值,而不用 new 一個對象(當(dāng)然也可以用 new)。所以String這個類型值得好好研究下。
Java中的變量和基本類型的值存放于棧內(nèi)存,而new出來的對象本身存放于堆內(nèi)存,指向?qū)ο蟮囊眠€是存放在棧內(nèi)存。例如如下的代碼:
int ?i=1;
String s = ?newString( "Hello World" );
變量i和s以及1存放在棧內(nèi)存,而s指向的對象”Hello World”存放于堆內(nèi)存。

棧內(nèi)存的一個特點(diǎn)是數(shù)據(jù)共享,這樣設(shè)計(jì)是為了減小內(nèi)存消耗,前面定義了i=1,i和1都在棧內(nèi)存內(nèi),如果再定義一個j=1,此時將j放入棧內(nèi)存,然后查找棧內(nèi)存中是否有1,如果有則j指向1。如果再給j賦值2,則在棧內(nèi)存中查找是否有2,如果沒有就在棧內(nèi)存中放一個2,然后j指向2。也就是如果常量在棧內(nèi)存中,就將變量指向該常量,如果沒有就在該棧內(nèi)存增加一個該常量,并將變量指向該常量。

如果j++,這時指向的變量并不會改變,而是在棧內(nèi)尋找新的常量(比原來的常量大1),如果棧內(nèi)存有則指向它,如果沒有就在棧內(nèi)存中加入此常量并將j指向它。這種基本類型之間比較大小和我們邏輯上判斷大小是一致的。如定義i和j是都賦值1,則i==j結(jié)果為true。==用于判斷兩個變量指向的地址是否一樣。i==j就是判斷i指向的1和j指向的1是同一個嗎?當(dāng)然是了。對于直接賦值的字符串常量(如String s=“Hello World”;中的Hello World)也是存放在棧內(nèi)存中,而new出來的字符串對象(即String對象)是存放在堆內(nèi)存中。如果定義String s=“Hello World”和String w=“Hello World”,s==w嗎?肯定是true,因?yàn)樗麄冎赶虻氖峭粋€Hello World。

堆內(nèi)存沒有數(shù)據(jù)共享的特點(diǎn),前面定義的String s = new String( “Hello World” );后,變量s在棧內(nèi)存內(nèi),Hello World 這個String對象在堆內(nèi)存內(nèi)。如果定義String w = new String( “Hello World” );,則會在堆內(nèi)存創(chuàng)建一個新的String對象,變量w存放在棧內(nèi)存,w指向這個新的String對象。堆內(nèi)存中不同對象(指同一類型的不同對象)的比較如果用==則結(jié)果肯定都是false,比如s==w?當(dāng)然不等,s和w指向堆內(nèi)存中不同的String對象。如果判斷兩個String對象相等呢?用equals方法。

說了這么多只是說了這道題的鋪墊知識,還沒進(jìn)入主題,下面分析這道題。MESSAGE 成員變量及其指向的字符串常量肯定都是在棧內(nèi)存里的,變量 a 運(yùn)算完也是指向一個字符串“ taobao ”啊?是不是同一個呢?這涉及到編譯器優(yōu)化問題。對于字符串常量的相加,在編譯時直接將字符串合并,而不是等到運(yùn)行時再合并。也就是說String a = “tao” + “bao” ;和String a = “taobao” ;編譯出的字節(jié)碼是一樣的。所以等到運(yùn)行時,根據(jù)上面說的棧內(nèi)存是數(shù)據(jù)共享原則,a和MESSAGE指向的是同一個字符串。而對于后面的(b+c)又是什么情況呢?b+c只能等到運(yùn)行時才能判定是什么字符串,編譯器不會優(yōu)化,想想這也是有道理的,編譯器怕你對b的值改變,所以編譯器不會優(yōu)化。運(yùn)行時b+c計(jì)算出來的"taobao"和棧內(nèi)存里已經(jīng)有的"taobao"是一個嗎?
不是。b+c計(jì)算出來的"taobao"應(yīng)該是放在堆內(nèi)存中的String對象。這可以通過System. out .println( (b+c)== MESSAGE );的結(jié)果為false來證明這一點(diǎn)。如果計(jì)算出來的b+c也是在棧內(nèi)存,那結(jié)果應(yīng)該是true。Java對String的相加是通過StringBuffer實(shí)現(xiàn)的,先構(gòu)造一個StringBuffer里面存放”tao”,然后調(diào)用append()方法追加”bao”,然后將值為”taobao”的StringBuffer轉(zhuǎn)化成String對象。StringBuffer對象在堆內(nèi)存中,那轉(zhuǎn)換成的String對象理所應(yīng)當(dāng)?shù)囊彩窃诙褍?nèi)存中。下面改造一下這個語句System. out .println( (b+c).intern()== MESSAGE );結(jié)果是true, intern() 方法會先檢查 String 池 ( 或者說成棧內(nèi)存 ) 中是否存在相同的字符串常量,如果有就返回。所以 intern()返回的就是MESSAGE指向的"taobao"。再把變量b和c的定義改一下,
nfinal ?String b = ?"tao" ;
final ?String c = ?"bao" ;
System. out .println( (b+c)== MESSAGE );
現(xiàn)在b和c不可能再次賦值了,所以編譯器將b+c編譯成了”taobao”。因此,這時的結(jié)果是true。
在字符串相加中,只要有一個是非final類型的變量,編譯器就不會優(yōu)化,因?yàn)檫@樣的變量可能發(fā)生改變,所以編譯器不可能將這樣的變量替換成常量。例如將變量b的final去掉,結(jié)果又變成了false。這也就意味著會用到StringBuffer對象,計(jì)算的結(jié)果在堆內(nèi)存中。
如果對指向堆內(nèi)存中的對象的String變量調(diào)用intern()會怎么樣呢?實(shí)際上這個問題已經(jīng)說過了,(b+c).intern(),b+c的結(jié)果就是在堆內(nèi)存中。對于指向棧內(nèi)存中字符串常量的變量調(diào)用intern()返回的還是它自己,沒有多大意義。它會根據(jù)堆內(nèi)存中對象的值,去查找String池中是否有相同的字符串,如果有就將變量指向這個string池中的變量。
String a = "tao"+"bao";
String b = newString("taobao");
System.out.println(a==MESSAGE); //true
System.out.println(b==MESSAGE); ?//false
b = b.intern();
System.out.println(b==MESSAGE); //true
System.out.println(a==a.intern()); ?//true
圣誕節(jié)快樂
總結(jié):
String 類繼承自 Object 超類,實(shí)現(xiàn)的接口有:Serializable、CharSequence、Comparable接口
字符串的分類,字符串分為可變的字符串和不可變的字符串兩種
String類型是引用類型,可以通過 String s1 = "abc";直接賦值進(jìn)行實(shí)例化,也可以通過new 關(guān)鍵字實(shí)例化,它也有自己的構(gòu)造函數(shù)。
創(chuàng)建對象的方式一:通過字面量賦值創(chuàng)建,String s1 = “l(fā)aofu”; 需要注意這里是雙引號:“”,區(qū)別與字符char類型的單引號:‘’;
方式二:通過構(gòu)造器創(chuàng)建, String s2 = new String(“l(fā)aofu”);
常用的方法:
計(jì)算字符串的長度
連接字符串
3.比較字符串
4.提取字符串
5.替換字符串
對象在jvm的位置和在常量池中的位置
在項(xiàng)目中對字符串的使用很常見,但是不要忘記了它是引用類型,不是基本類型不可以做存儲和數(shù)據(jù)類型操作
面試中也有很多關(guān)于string 的題型,比如==和equals()、++或者-- 等,好好敲代碼,掌握好string。
