- 看看源碼
大家都知道, String 被聲明為 final,因此它不可被繼承。(Integer 等包裝類也不能被繼承)。我們先來看看 String 的源碼。
在 Java 8 中,String 內部使用 char 數組存儲數據。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
在 Java 9 之后,String 類的實現改用 byte 數組存儲字符串,同時使用 coder 來標識使用了哪種編碼。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;
/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}
value 數組被聲明為 final,這意味著 value 數組初始化之后就不能再引用其它數組。并且 String 內部沒有改變 value 數組的方法,因此可以保證 String 不可變。
2. 不可變有什么好處呢
2.1 可以緩存 hash 值
因為 String 的 hash 值經常被使用,例如 String 用做 HashMap 的 key。不可變的特性可以使得 hash 值也不可變,因此只需要進行一次計算。
2.2 String Pool 的使用
如果一個 String 對象已經被創建過了,那么就會從 String Pool 中取得引用。只有 String 是不可變的,才可能使用 String Pool。
2.3 安全性
String 經常作為參數,String 不可變性可以保證參數不可變。例如在作為網絡連接參數的情況下如果 String 是可變的,那么在網絡連接過程中,String 被改變,改變 String 的那一方以為現在連接的是其它主機,而實際情況卻不一定是。
2.4 線程安全
String 不可變性天生具備線程安全,可以在多個線程中安全地使用。
3. 再來深入了解一下 String
3.1 “+” 連接符
字符串對象可以使用“+”連接其他對象,其中字符串連接是通過 StringBuilder(或 StringBuffer)類及其 append 方法實現的,對象轉換為字符串是通過 toString 方法實現的。可以通過反編譯驗證一下:
/**
* 測試代碼
*/
public class Test {
public static void main(String[] args) {
int i = 10;
String s = "abc";
System.out.println(s + i);
}
}
/**
* 反編譯后
*/
public class Test {
public static void main(String args[]) { //刪除了默認構造函數和字節碼
byte byte0 = 10;
String s = "abc";
System.out.println((new StringBuilder()).append(s).append(byte0).toString());
}
}
由上可以看出,Java中使用"+"連接字符串對象時,會創建一個StringBuilder()對象,并調用append()方法將數據拼接,最后調用toString()方法返回拼接好的字符串。那這個 “+” 的效率怎么樣呢?
3.2 “+”連接符的效率
使用“+”連接符時,JVM會隱式創建StringBuilder對象,這種方式在大部分情況下并不會造成效率的損失,不過在進行大量循環拼接字符串時則需要注意。比如:
String s = "abc";
for (int i=0; i<10000; i++) {
s += "abc";
}
這樣由于大量StringBuilder創建在堆內存中,肯定會造成效率的損失,所以在這種情況下建議在循環體外創建一個StringBuilder對象調用append()方法手動拼接(如上面例子如果使用手動拼接運行時間將縮小到1/200左右)。
與此之外還有一種特殊情況,也就是當"+"兩端均為編譯期確定的字符串常量時,編譯器會進行相應的優化,直接將兩個字符串常量拼接好,例如:
System.out.println("Hello" + "World");
/**
* 反編譯后
*/
System.out.println("HelloWorld");
4. 字符串常量
4.1 為什么使用字符串常量?
JVM為了提高性能和減少內存的開銷,在實例化字符串的時候進行了一些優化:使用字符串常量池。每當創建字符串常量時,JVM會首先檢查字符串常量池,如果該字符串已經存在常量池中,那么就直接返回常量池中的實例引用。如果字符串不存在常量池中,就會實例化該字符串并且將其放到常量池中。由于String字符串的不可變性,常量池中一定不存在兩個相同的字符串。
4.2 實現字符串常量池的基礎
實現該優化的基礎是因為字符串是不可變的,可以不用擔心數據沖突進行共享。
運行時實例創建的全局字符串常量池中有一個表,總是為池中每個唯一的字符串對象維護一個引用,這就意味著它們一直引用著字符串常量池中的對象,所以,在常量池中的這些字符串不會被垃圾收集器回收。
我們來看個小例子,了解下不同的方式創建的字符串在內存中的位置:
String string1 = "abc"; // 常量池
String string2 = "abc"; // 常量池
String string3 = new String("abc"); // 堆內存
5. String類常見的面試題
5.1 判斷字符串是否相等
public static void main(String[] args) {
String s1 = "123";
String s2 = "123";
String s3 = "1234";
String s4 = "12" + "34";
String s5 = s1 + "4";
String s6 = new String("1234");
System.out.println(s1 == s2); // true
System.out.println(s1.equals(s2)); //true
System.out.println(s3 == s4); //true
System.out.println(s3 == s5); // false
System.out.println(s3.equals(s5)); //true
System.out.println(s3 == s6); // false
}
解析:
s1和s2:
String s1 = "123";先是在字符串常量池創建了一個字符串常量“123”,“123”常量是有地址值,地址值賦值給s1。接著聲明 String s2=“123”,由于s1已經在方法區的常量池創建字符串常量"123",進入常量池規則:如果常量池中沒有這個常量,就創建一個,如果有就不再創建了,故直接把常量"123"的地址值賦值給s2,所以s1==s2為true。
由于String類重寫了equals方法,s1.equals(s2)比較的是字符串的內容,s1和s2的內容都是"123",故s1.equals(s2)為true。
s3和s4:
s3創建了一個新的字符串"1234",s4是兩個新的字符串"12"和"34"通過"+“符號連接所得,根據Java中常量優化機制, “12” 和"34"兩個字符串常量在編譯期就連接創建了字符串"1234”,由于字符串"1234"在常量池中存在,故直接把"1234"在常量池的地址賦值給s4,所以s3==s4為true。
s3和s5:
s5是由一個變量s1連接一個新的字符串"4",首先會在常量池創建字符串"4",然后進行"+“操作,根據字符串的串聯規則,s5會在堆內存中創建StringBuilder(或StringBuffer)對象,通過append方法拼接s1和字符串常量"4”,此時拼接成的字符串"1234"是StringBuilder(或StringBuffer)類型的對象,通過調用toString方法轉成String對象"1234",所以s5此時實際指向的是堆內存中的"1234"對象,堆內存中對象的地址和常量池中對象的地址不一致,故s3==s5為false。
看下JDK8的API文檔里的解釋:
Java語言為字符串連接運算符(+)提供特殊支持,并為其他對象轉換為字符串。字符串連接是通過StringBuilder (或StringBuffer )類及其append方法實現的。字符串轉換是通過方法來實現toString,由下式定義0bject和繼承由在Java中的所有類。有關字符串連接和轉換的其他信息,請參閱Gosling,Joy 和Steele,Java 語言規范。
不管是常量池還是堆,只要是使用equals比較字符串,都是比較字符串的內容,所以s3.equals(s5)為true。
Java常量優化機制:給一個變量賦值,如果等于號的右邊是常量,并且沒有一個變量,那么就會在編譯階段計算該表達式的結果,然后判斷該表達式的結果是否在左邊類型所表示范圍內,如果在,那么就賦值成功,如果不在,那么就賦值失敗。但是注意如果一旦有變量參與表達式,那么就不會有編譯期間的常量優化機制。
s3和s6:
String s6 = new String("1234");在堆內存創建一個字符串對象,s6指向這個堆內存的對象地址,而s3指向的是字符串常量池的"1234"對象的地址,故s3==s6為false。
5.2 創建多少個字符串對象?
String s0 = "123";
String s1 = new String("123");
String s2 = new String("1" + "2");
String s3 = new String("12") + "3";
解析:
String s0 = “123”;
字符串常量池對象:“123”,1個;
共1個。
String s1 = new String(“123”);
字符串常量池對象:“123”,1個;
堆對象:new String(“123”),1個;
共2個。
String s2 = new String(“1” + “2”);
字符串常量池對象:“12”,1個(Jvm在編譯期做了優化,“1” + "2"合并成了 “12”);
堆對象:new String(“12”),1個
共2個。
由于s2涉及字符串合并,我們通過命令看下字節碼信息:
javac StrTest.java //編譯源文件得到class文件
javap -c StrTest.class // 查看編譯結果
得到字節碼信息如下:
備注:以上編譯結果基于Jdk1.8運行環境
我們可以很清晰看到,創建了一個新的String對象和一個字符串常量"12",new String("1" + "2") 相當于 new String("12"),共創建了2個字符串對象。
String s3 = new String(“12”) + “3”;
字符串常量池對象:“12”、“3”,2個,
堆對象: new Stringbuilder().append(“12”).append(“3”).toString();轉成String對象,1個;
共3個。
我們同樣看下編譯后的結果:
可以看到,包括StringBuilder在內,共創建了4個對象,字符串"12"和字符串"3"是分開創建的,所以共創建了3個字符串對象。
總結:
new String()是在堆內存創建新的字符串對象,其構造參數中可傳入字符串,此字符串一般會在常量池中先創建出來,new String()創建的字符串是參數字符串的副本,看下API中關于String構造器的解釋:
String(String original)
初始化新創建的String對象,使其表示與參數相同的字符序列;換句話說,新創建的字符串是參數字符串的副本。
所以new String()的方式創建字符串百分百會產生一個新的字符串對象,而類似于"123"這樣的字符串對象則需要在創建之前看常量池中有沒有,有的話就不創建,沒有則創建新的對象。 "+"操作符連接字符串常量的時候會在編譯期直接生成連接后的字符串,若該字符串在常量池已經存在,則不會創建新的字符串;連接變量的話則涉及StringBuilder等字符串構建器的創建,會在堆內存生成新的字符串對象。
以上就是我們給您帶來的關于Java字符串的一些知識總結和面試技巧,你學廢了嗎?