1、String類java
想要了解一個類,最好的辦法就是看這個類的實現源代碼,來看一下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; ........ }
從上面能夠看出幾點:面試
1)String類是final類,也即意味着String類不能被繼承,而且它的成員方法都默認爲final方法。在Java中,被final修飾的類是不容許被繼承的,而且該類中的成員方法都默認爲final方法。spring
2)上面列舉出了String類中全部的成員屬性,從上面能夠看出String類實際上是經過char數組來保存字符串的。數組
下面再繼續看String類的一些方法實現:安全
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) { int otherLen = str.length(); if (otherLen == 0) { return this; } char buf[] = new char[count + otherLen]; getChars(0, count, buf, 0); str.getChars(0, otherLen, buf, count); 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對象一旦被建立就是固定不變的了,對String對象的任何改變都不影響到原對象,相關的任何change操做都會生成新的對象」。app
2、字符串常量池jvm
咱們知道字符串的分配和其餘對象分配同樣,是須要消耗高昂的時間和空間的,並且字符串咱們使用的很是多。JVM爲了提升性能和減小內存的開銷,在實例化字符串的時候進行了一些優化:使用字符串常量池。每當咱們建立字符串常量時,JVM會首先檢查字符串常量池,若是該字符串已經存在常量池中,那麼就直接返回常量池中的實例引用。若是字符串不存在常量池中,就會實例化該字符串而且將其放到常量池中。因爲String字符串的不可變性咱們能夠十分確定常量池中必定不存在兩個相同的字符串(這點對理解上面相當重要)。ide
Java中的常量池,實際上分爲兩種形態:靜態常量池和運行時常量池。
所謂靜態常量池,即*.class文件中的常量池,class文件中的常量池不只僅包含字符串(數字)字面量,還包含類、方法的信息,佔用class文件絕大部分空間。
而運行時常量池,則是jvm虛擬機在完成類裝載操做後,將class文件中的常量池載入到內存中,並保存在方法區中,咱們常說的常量池,就是指方法區中的運行時常量池。
來看下面的程序:
String a = "chenssy";
String b = "chenssy";
a、b和字面上的chenssy都是指向JVM字符串常量池中的"chenssy"對象,他們指向同一個對象。
String c = new String("chenssy");
new關鍵字必定會產生一個對象chenssy(注意這個chenssy和上面的chenssy不一樣),同時這個對象是存儲在堆中。因此上面應該產生了兩個對象:保存在棧中的c和保存堆中chenssy。可是在Java中根本就不存在兩個徹底如出一轍的字符串對象。故堆中的chenssy應該是引用字符串常量池中chenssy。因此c、chenssy、池chenssy的關係應該是:c--->chenssy--->池chenssy。整個關係以下:
經過上面的圖咱們能夠很是清晰的認識他們之間的關係。因此咱們修改內存中的值,他變化的是全部。
總結:雖然a、b、c、chenssy是不一樣的對象,可是從String的內部結構咱們是能夠理解上面的。String c = new String("chenssy");雖然c的內容是建立在堆中,可是他的內部value仍是指向JVM常量池的chenssy的value,它構造chenssy時所用的參數依然是chenssy字符串常量。
下面再來看幾個例子:
例子1:
/** * 採用字面值的方式賦值 */ public void test1(){ String str1="aaa"; String str2="aaa"; System.out.println("===========test1============"); System.out.println(str1==str2);//true 能夠看出str1跟str2是指向同一個對象 }
執行上述代碼,結果爲:true。
分析:當執行String str1="aaa"時,JVM首先會去字符串池中查找是否存在"aaa"這個對象,若是不存在,則在字符串池中建立"aaa"這個對象,而後將池中"aaa"這個對象的引用地址返回給字符串常量str1,這樣str1會指向池中"aaa"這個字符串對象;若是存在,則不建立任何對象,直接將池中"aaa"這個對象的地址返回,賦給字符串常量。當建立字符串對象str2時,字符串池中已經存在"aaa"這個對象,直接把對象"aaa"的引用地址返回給str2,這樣str2指向了池中"aaa"這個對象,也就是說str1和str2指向了同一個對象,所以語句System.out.println(str1 == str2)輸出:true。
例子2:
/** * 採用new關鍵字新建一個字符串對象 */ public void test2(){ String str3=new String("aaa"); String str4=new String("aaa"); System.out.println("===========test2============"); System.out.println(str3==str4);//false 能夠看出用new的方式是生成不一樣的對象 }
執行上述代碼,結果爲:false。
分析: 採用new關鍵字新建一個字符串對象時,JVM首先在字符串池中查找有沒有"aaa"這個字符串對象,若是有,則不在池中再去建立"aaa"這個對象了,直接在堆中建立一個"aaa"字符串對象,而後將堆中的這個"aaa"對象的地址返回賦給引用str3,這樣,str3就指向了堆中建立的這個"aaa"字符串對象;若是沒有,則首先在字符串池中建立一個"aaa"字符串對象,而後再在堆中建立一個"aaa"字符串對象,而後將堆中這個"aaa"字符串對象的地址返回賦給str3引用,這樣,str3指向了堆中建立的這個"aaa"字符串對象。當執行String str4=new String("aaa")時, 由於採用new關鍵字建立對象時,每次new出來的都是一個新的對象,也便是說引用str3和str4指向的是兩個不一樣的對象,所以語句System.out.println(str3 == str4)輸出:false。
例子3:
/** * 編譯期肯定 */ public void test3(){ String s0="helloworld"; String s1="helloworld"; String s2="hello"+"world"; System.out.println("===========test3============"); System.out.println(s0==s1); //true 能夠看出s0跟s1是指向同一個對象 System.out.println(s0==s2); //true 能夠看出s0跟s2是指向同一個對象 }
執行上述代碼,結果爲:true、true。
分析:由於例子中的s0和s1中的"helloworld」都是字符串常量,它們在編譯期就被肯定了,因此s0==s1爲true;而"hello」和"world」也都是字符串常量,當一個字符串由多個字符串常量鏈接而成時,它本身確定也是字符串常量,因此s2也一樣在編譯期就被解析爲一個字符串常量,因此s2也是常量池中"helloworld」的一個引用。因此咱們得出s0==s1==s2。
例子4:
/** * 編譯期沒法肯定 */ public void test4(){ String s0="helloworld"; String s1=new String("helloworld"); String s2="hello" + new String("world"); System.out.println("===========test4============"); System.out.println( s0==s1 ); //false System.out.println( s0==s2 ); //false System.out.println( s1==s2 ); //false }
執行上述代碼,結果爲:false、false、false。
分析:用new String() 建立的字符串不是常量,不能在編譯期就肯定,因此new String() 建立的字符串不放入常量池中,它們有本身的地址空間。
s0仍是常量池中"helloworld」的引用,s1由於沒法在編譯期肯定,因此是運行時建立的新對象"helloworld」的引用,s2由於有後半部分new String(」world」)因此也沒法在編譯期肯定,因此也是一個新建立對象"helloworld」的引用。
例子5:
/** * 繼續-編譯期沒法肯定 */ public void test5(){ String str1="abc"; String str2="def"; String str3=str1+str2; System.out.println("===========test5============"); System.out.println(str3=="abcdef"); //false }
執行上述代碼,結果爲:false。
分析:由於str3指向堆中的"abcdef"對象,而"abcdef"是字符串池中的對象,因此結果爲false。JVM對String str="abc"對象放在常量池中是在編譯時作的,而String str3=str1+str2是在運行時刻才能知道的。new對象也是在運行時才作的。而這段代碼總共建立了5個對象,字符串池中兩個、堆中三個。+運算符會在堆中創建來兩個String對象,這兩個對象的值分別是"abc"和"def",也就是說從字符串池中複製這兩個值,而後在堆中建立兩個對象,而後再創建對象str3,而後將"abcdef"的堆地址賦給str3。
步驟:
1)棧中開闢一塊中間存放引用str1,str1指向池中String常量"abc"。
2)棧中開闢一塊中間存放引用str2,str2指向池中String常量"def"。
3)棧中開闢一塊中間存放引用str3。
4)str1 + str2經過StringBuilder的最後一步toString()方法還原一個新的String對象"abcdef",所以堆中開闢一塊空間存放此對象。
5)引用str3指向堆中(str1 + str2)所還原的新String對象。
6)str3指向的對象在堆中,而常量"abcdef"在池中,輸出爲false。
例子6:
/** * 編譯期優化 */ public void test6(){ String s0 = "a1"; String s1 = "a" + 1; System.out.println("===========test6============"); System.out.println((s0 == s1)); //result = true String s2 = "atrue"; String s3= "a" + "true"; System.out.println((s2 == s3)); //result = true String s4 = "a3.4"; String s5 = "a" + 3.4; System.out.println((s4 == s5)); //result = true }
執行上述代碼,結果爲:true、true、true。
分析:在程序編譯期,JVM就將常量字符串的"+"鏈接優化爲鏈接後的值,拿"a" + 1來講,經編譯器優化後在class中就已是a1。在編譯期其字符串常量的值就肯定下來,故上面程序最終的結果都爲true。
例子7:
/** * 編譯期沒法肯定 */ public void test7(){ String s0 = "ab"; String s1 = "b"; String s2 = "a" + s1; System.out.println("===========test7============"); System.out.println((s0 == s2)); //result = false }
執行上述代碼,結果爲:false。
分析:JVM對於字符串引用,因爲在字符串的"+"鏈接中,有字符串引用存在,而引用的值在程序編譯期是沒法肯定的,即"a" + s1沒法被編譯器優化,只有在程序運行期來動態分配並將鏈接後的新地址賦給s2。因此上面程序的結果也就爲false。
例子8:
/** * 比較字符串常量的「+」和字符串引用的「+」的區別 */ public void test8(){ String test="javalanguagespecification"; String str="java"; String str1="language"; String str2="specification"; System.out.println("===========test8============"); System.out.println(test == "java" + "language" + "specification"); System.out.println(test == str + str1 + str2); }
執行上述代碼,結果爲:true、false。
分析:爲何出現上面的結果呢?這是由於,字符串字面量拼接操做是在Java編譯器編譯期間就執行了,也就是說編譯器編譯時,直接把"java"、"language"和"specification"這三個字面量進行"+"操做獲得一個"javalanguagespecification" 常量,而且直接將這個常量放入字符串池中,這樣作其實是一種優化,將3個字面量合成一個,避免了建立多餘的字符串對象。而字符串引用的"+"運算是在Java運行期間執行的,即str + str2 + str3在程序執行期間纔會進行計算,它會在堆內存中從新建立一個拼接後的字符串對象。總結來講就是:字面量"+"拼接是在編譯期間進行的,拼接後的字符串存放在字符串池中;而字符串引用的"+"拼接運算實在運行時進行的,新建立的字符串存放在堆中。
對於直接相加字符串,效率很高,由於在編譯器便肯定了它的值,也就是說形如"I"+"love"+"java"; 的字符串相加,在編譯期間便被優化成了"Ilovejava"。對於間接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,由於在編譯器不會對引用變量進行優化。
例子9:
/** * 編譯期肯定 */ public void test9(){ String s0 = "ab"; final String s1 = "b"; String s2 = "a" + s1; System.out.println("===========test9============"); System.out.println((s0 == s2)); //result = true }
執行上述代碼,結果爲:true。
分析:和例子7中惟一不一樣的是s1字符串加了final修飾,對於final修飾的變量,它在編譯時被解析爲常量值的一個本地拷貝存儲到本身的常量池中或嵌入到它的字節碼流中。因此此時的"a" + s1和"a" + "b"效果是同樣的。故上面程序的結果爲true。
例子10:
/** * 編譯期沒法肯定 */ public void test10(){ String s0 = "ab"; final String s1 = getS1(); String s2 = "a" + s1; System.out.println("===========test10============"); System.out.println((s0 == s2)); //result = false } private static String getS1() { return "b"; }
執行上述代碼,結果爲:false。
分析:這裏面雖然將s1用final修飾了,可是因爲其賦值是經過方法調用返回的,那麼它的值只能在運行期間肯定,所以s0和s2指向的不是同一個對象,故上面程序的結果爲false。
3、總結
1.String類初始化後是不可變的(immutable)
String使用private final char value[]來實現字符串的存儲,也就是說String對象建立以後,就不能再修改此對象中存儲的字符串內容,就是由於如此,才說String類型是不可變的(immutable)。程序員不能對已有的不可變對象進行修改。咱們本身也能夠建立不可變對象,只要在接口中不提供修改數據的方法就能夠。
然而,String類對象確實有編輯字符串的功能,好比replace()。這些編輯功能是經過建立一個新的對象來實現的,而不是對原有對象進行修改。好比:
s = s.replace("World", "Universe");
上面對s.replace()的調用將建立一個新的字符串"Hello Universe!",並返回該對象的引用。經過賦值,引用s將指向該新的字符串。若是沒有其餘引用指向原有字符串"Hello World!",原字符串對象將被垃圾回收。
2.引用變量與對象
A aa;
這個語句聲明一個類A的引用變量aa[咱們經常稱之爲句柄],而對象通常經過new建立。因此aa僅僅是一個引用變量,它不是對象。
3.建立字符串的方式
建立字符串的方式概括起來有兩類:
(1)使用""引號建立字符串;
(2)使用new關鍵字建立字符串。
結合上面例子,總結以下:
(1)單獨使用""引號建立的字符串都是常量,編譯期就已經肯定存儲到String Pool中;
(2)使用new String("")建立的對象會存儲到heap中,是運行期新建立的;
new建立字符串時首先查看池中是否有相同值的字符串,若是有,則拷貝一份到堆中,而後返回堆中的地址;若是池中沒有,則在堆中建立一份,而後返回堆中的地址(注意,此時不須要從堆中複製到池中,不然,將使得堆中的字符串永遠是池中的子集,致使浪費池的空間)!
(3)使用只包含常量的字符串鏈接符如"aa" + "aa"建立的也是常量,編譯期就能肯定,已經肯定存儲到String Pool中;
(4)使用包含變量的字符串鏈接符如"aa" + s1建立的對象是運行期才建立的,存儲在heap中;
4.使用String不必定建立對象
在執行到雙引號包含字符串的語句時,如String a = "123",JVM會先到常量池裏查找,若是有的話返回常量池裏的這個實例的引用,不然的話建立一個新實例並置入常量池裏。因此,當咱們在使用諸如String str = "abc";的格式定義對象時,老是想固然地認爲,建立了String類的對象str。擔憂陷阱!對象可能並無被建立!而可能只是指向一個先前已經建立的對象。只有經過new()方法才能保證每次都建立一個新的對象。
5.使用new String,必定建立對象
在執行String a = new String("123")的時候,首先走常量池的路線取到一個實例的引用,而後在堆上建立一個新的String實例,走如下構造函數給value屬性賦值,而後把實例引用賦值給a:
public String(String original) { int size = original.count; char[] originalValue = original.value; char[] v; if (originalValue.length > size) { // The array representing the String is bigger than the new // String itself. Perhaps this constructor is being called // in order to trim the baggage, so make a copy of the array. int off = original.offset; v = Arrays.copyOfRange(originalValue, off, off+size); } else { // The array representing the String is the same // size as the String, so no point in making a copy. v = originalValue; } this.offset = 0; this.count = size; this.value = v; }
從中咱們能夠看到,雖然是新建立了一個String的實例,可是value是等於常量池中的實例的value,便是說沒有new一個新的字符數組來存放"123"。
6.關於String.intern()
intern方法使用:一個初始爲空的字符串池,它由類String獨自維護。當調用 intern方法時,若是池已經包含一個等於此String對象的字符串(用equals(oject)方法肯定),則返回池中的字符串。不然,將此String對象添加到池中,並返回此String對象的引用。
它遵循如下規則:對於任意兩個字符串 s 和 t,當且僅當 s.equals(t) 爲 true 時,s.intern() == t.intern() 才爲 true。
String.intern();
再補充介紹一點:存在於.class文件中的常量池,在運行期間被jvm裝載,而且能夠擴充。String的intern()方法就是擴充常量池的一個方法;當一個String實例str調用intern()方法時,java查找常量池中是否有相同unicode的字符串常量,若是有,則返回其引用,若是沒有,則在常量池中增長一個unicode等於str的字符串並返回它的引用。
/** * 關於String.intern() */ public void test11(){ String s0 = "kvill"; String s1 = new String("kvill"); String s2 = new String("kvill"); System.out.println("===========test11============"); System.out.println( s0 == s1 ); //false System.out.println( "**********" ); s1.intern(); //雖然執行了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 }
運行結果:false、false、true、true。
7.關於equals和==
(1)對於==,若是做用於基本數據類型的變量(byte,short,char,int,long,float,double,boolean ),則直接比較其存儲的"值"是否相等;若是做用於引用類型的變量(String),則比較的是所指向的對象的地址(便是否指向同一個對象)。
(2)equals方法是基類Object中的方法,所以對於全部的繼承於Object的類都會有該方法。在Object類中,equals方法是用來比較兩個對象的引用是否相等,便是否指向同一個對象。
(3)對於equals方法,注意:equals方法不能做用於基本數據類型的變量。若是沒有對equals方法進行重寫,則比較的是引用類型的變量所指向的對象的地址;而String類對equals方法進行了重寫,用來比較指向的字符串對象所存儲的字符串是否相等。其餘的一些類諸如Double,Date,Integer等,都對equals方法進行了重寫用來比較指向的對象所存儲的內容是否相等。
/** * 關於equals和== */ public void test12(){ String s1="hello"; String s2="hello"; String s3=new String("hello"); System.out.println("===========test12============"); System.out.println( s1 == s2); //true,表示s1和s2指向同一對象,它們都指向常量池中的"hello"對象 //flase,表示s1和s3的地址不一樣,即它們分別指向的是不一樣的對象,s1指向常量池中的地址,s3指向堆中的地址 System.out.println( s1 == s3); System.out.println( s1.equals(s3)); //true,表示s1和s3所指向對象的內容相同 }
8.String相關的+:
String中的 + 經常使用於字符串的鏈接。看下面一個簡單的例子:
/** * String相關的+ */ public void test13(){ String a = "aa"; String b = "bb"; String c = "xx" + "yy " + a + "zz" + "mm" + b; System.out.println("===========test13============"); System.out.println(c); }
編譯運行後,主要字節碼部分以下:
public static main([Ljava/lang/String;)V L0 LINENUMBER 5 L0 LDC "aa" ASTORE 1 L1 LINENUMBER 6 L1 LDC "bb" ASTORE 2 L2 LINENUMBER 7 L2 NEW java/lang/StringBuilder DUP LDC "xxyy " INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V ALOAD 1 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; LDC "zz" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; LDC "mm" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ALOAD 2 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; ASTORE 3 L3 LINENUMBER 8 L3 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; ALOAD 3 INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L4 LINENUMBER 9 L4 RETURN L5 LOCALVARIABLE args [Ljava/lang/String; L0 L5 0 LOCALVARIABLE a Ljava/lang/String; L1 L5 1 LOCALVARIABLE b Ljava/lang/String; L2 L5 2 LOCALVARIABLE c Ljava/lang/String; L3 L5 3 MAXSTACK = 3 MAXLOCALS = 4 }
顯然,經過字節碼咱們能夠得出以下幾點結論:
(1).String中使用 + 字符串鏈接符進行字符串鏈接時,鏈接操做最開始時若是都是字符串常量,編譯後將盡量多的直接將字符串常量鏈接起來,造成新的字符串常量參與後續鏈接(經過反編譯工具jd-gui也能夠方便的直接看出);
(2).接下來的字符串鏈接是從左向右依次進行,對於不一樣的字符串,首先以最左邊的字符串爲參數建立StringBuilder對象,而後依次對右邊進行append操做,最後將StringBuilder對象經過toString()方法轉換成String對象(注意:中間的多個字符串常量不會自動拼接)。
也就是說String c = "xx" + "yy " + a + "zz" + "mm" + b; 實質上的實現過程是: String c = new StringBuilder("xxyy ").append(a).append("zz").append("mm").append(b).toString();
由此得出結論:當使用+進行多個字符串鏈接時,其實是產生了一個StringBuilder對象和一個String對象。
9.String的不可變性致使字符串變量使用+號的代價:
String s = "a" + "b" + "c"; String s1 = "a"; String s2 = "b"; String s3 = "c"; String s4 = s1 + s2 + s3;
分析:變量s的建立等價於 String s = "abc"; 由上面例子可知編譯器進行了優化,這裏只建立了一個對象。由上面的例子也能夠知道s4不能在編譯期進行優化,其對象建立至關於:
StringBuilder temp = new StringBuilder(); temp.append(a).append(b).append(c); String s = temp.toString();
由上面的分析結果,可就不難推斷出String 採用鏈接運算符(+)效率低下緣由分析,形如這樣的代碼:
public class Test { public static void main(String args[]) { String s = null; for(int i = 0; i < 100; i++) { s += "a"; } } }
每作一次 + 就產生個StringBuilder對象,而後append後就扔掉。下次循環再到達時從新產生個StringBuilder對象,而後 append 字符串,如此循環直至結束。 若是咱們直接採用 StringBuilder 對象進行 append 的話,咱們能夠節省 N - 1 次建立和銷燬對象的時間。因此對於在循環中要進行字符串鏈接的應用,通常都是用StringBuffer或StringBulider對象來進行append操做。
10.String、StringBuffer、StringBuilder的區別
(1)可變與不可變:String是不可變字符串對象,StringBuilder和StringBuffer是可變字符串對象(其內部的字符數組長度可變)。
(2)是否多線程安全:String中的對象是不可變的,也就能夠理解爲常量,顯然線程安全。StringBuffer 與 StringBuilder 中的方法和功能徹底是等價的,只是StringBuffer 中的方法大都採用了synchronized 關鍵字進行修飾,所以是線程安全的,而 StringBuilder 沒有這個修飾,能夠被認爲是非線程安全的。
(3)String、StringBuilder、StringBuffer三者的執行效率:
StringBuilder > StringBuffer > String 固然這個是相對的,不必定在全部狀況下都是這樣。好比String str = "hello"+ "world"的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高。所以,這三個類是各有利弊,應當根據不一樣的狀況來進行選擇使用:
當字符串相加操做或者改動較少的狀況下,建議使用 String str="hello"這種形式;
當字符串相加操做較多的狀況下,建議使用StringBuilder,若是採用了多線程,則使用StringBuffer。
11.String中的final用法和理解
final StringBuffer a = new StringBuffer("111"); final StringBuffer b = new StringBuffer("222"); a=b;//此句編譯不經過 final StringBuffer a = new StringBuffer("111"); a.append("222");//編譯經過
可見,final只對引用的"值"(即內存地址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會致使編譯期錯誤。至於它所指向的對象的變化,final是不負責的。
12.關於String str = new String("abc")建立了多少個對象?
這個問題在不少書籍上都有說到好比《Java程序員面試寶典》,包括不少國內大公司筆試面試題都會遇到,大部分網上流傳的以及一些面試書籍上都說是2個對象,這種說法是片面的。
首先必須弄清楚建立對象的含義,建立是何時建立的?這段代碼在運行期間會建立2個對象麼?毫無疑問不可能,用javap -c反編譯便可獲得JVM執行的字節碼內容:
很顯然,new只調用了一次,也就是說只建立了一個對象。而這道題目讓人混淆的地方就是這裏,這段代碼在運行期間確實只建立了一個對象,即在堆上建立了"abc"對象。而爲何你們都在說是2個對象呢,這裏面要澄清一個概念,該段代碼執行過程和類的加載過程是有區別的。在類加載的過程當中,確實在運行時常量池中建立了一個"abc"對象,而在代碼執行過程當中確實只建立了一個String對象。
所以,這個問題若是換成 String str = new String("abc")涉及到幾個String對象?合理的解釋是2個。
我的以爲在面試的時候若是遇到這個問題,能夠向面試官詢問清楚」是這段代碼執行過程當中建立了多少個對象仍是涉及到多少個對象「再根據具體的來進行回答。
13.字符串池的優缺點:
字符串池的優勢就是避免了相同內容的字符串的建立,節省了內存,省去了建立相同字符串的時間,同時提高了性能;另外一方面,字符串池的缺點就是犧牲了JVM在常量池中遍歷對象所須要的時間,不過其時間成本相比而言比較低。
4、綜合實例
package com.spring.test; public class StringTest { public static void main(String[] args) { /** * 情景一:字符串池 * JAVA虛擬機(JVM)中存在着一個字符串池,其中保存着不少String對象; * 而且能夠被共享使用,所以它提升了效率。 * 因爲String類是final的,它的值一經建立就不可改變。 * 字符串池由String類維護,咱們能夠調用intern()方法來訪問字符串池。 */ String s1 = "abc"; //↑ 在字符串池建立了一個對象 String s2 = "abc"; //↑ 字符串pool已經存在對象「abc」(共享),因此建立0個對象,累計建立一個對象 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"); //↑ 建立了兩個對象,一個存放在字符串池中,一個存在與堆區中; //↑ 還有一個對象引用s3存放在棧中 String s4 = new String("abc"); //↑ 字符串池中已經存在「abc」對象,因此只在堆中建立了一個對象 System.out.println("s3 == s4 : "+(s3==s4)); //↑false s3和s4棧區的地址不一樣,指向堆區的不一樣地址; System.out.println("s3.equals(s4) : "+(s3.equals(s4))); //↑true s3和s4的值相同 System.out.println("s1 == s3 : "+(s1==s3)); //↑false 存放的地區多不一樣,一個棧區,一個堆區 System.out.println("s1.equals(s3) : "+(s1.equals(s3))); //↑true 值相同 //↑------------------------------------------------------over /** * 情景三: * 因爲常量的值在編譯的時候就被肯定(優化)了。 * 在這裏,"ab"和"cd"都是常量,所以變量str3的值在編譯時就能夠肯定。 * 這行代碼編譯後的效果等同於: String str3 = "abcd"; */ String str1 = "ab" + "cd"; //1個對象 String str11 = "abcd"; System.out.println("str1 = str11 : "+ (str1 == str11)); //↑------------------------------------------------------over /** * 情景四: * 局部變量str2,str3存儲的是存儲兩個拘留字符串對象(intern字符串對象)的地址。 * * 第三行代碼原理(str2+str3): * 運行期JVM首先會在堆中建立一個StringBuilder類, * 同時用str2指向的拘留字符串對象完成初始化, * 而後調用append方法完成對str3所指向的拘留字符串的合併, * 接着調用StringBuilder的toString()方法在堆中建立一個String對象, * 最後將剛生成的String對象的堆地址存放在局部變量str3中。 * * 而str5存儲的是字符串池中"abcd"所對應的拘留字符串對象的地址。 * str4與str5地址固然不同了。 * * 內存中實際上有五個字符串對象: * 三個拘留字符串對象、一個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 + 基本類型/常量 是當成常量表達式直接求值來優化的。 * 運行期的兩個string相加,會產生新的對象的,存儲在堆(heap)中 */ String str6 = "b"; String str7 = "a" + str6; String str67 = "ab"; System.out.println("str7 = str67 : "+ (str7 == str67)); //↑str6爲變量,在運行期纔會被解析。 final String str8 = "b"; String str9 = "a" + str8; String str89 = "ab"; System.out.println("str9 = str89 : "+ (str9 == str89)); //↑str8爲常量變量,編譯期會被優化 //↑------------------------------------------------------over } }
運行結果:
s1 == s2 : trues1.equals(s2) : trues3 == s4 : falses3.equals(s4) : trues1 == s3 : falses1.equals(s3) : truestr1 = str11 : truestr4 = str5 : falsestr7 = str67 : falsestr9 = str89 : true