我一個5年的Java程序員居然倒在了String面試上……

本文是我和xqnode聯合創作,已收錄至我們的GitHub,歡迎大家給個Star:https://github.com/nxJava/nx_java

我們會持續更新,歡迎監督!

微信搜索:武哥聊編程,關注這個Java菜鳥~

今天跟大家聊聊Java中的String,這裏面門道還挺多的,由於本人能力有限,如果有寫的不對的地方,歡迎大家給我指正,謝謝支持~


1. 看看源碼

大家都知道, 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 判斷字符串s1和s2是否相等

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文檔裏的解釋:

    不管是常量池還是堆,只要是使用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";

解析:

  • s0:

    符串常量池對象:“123”,1個;

    共1個。

  • s1:

    字符串常量池對象:“123”,1個;

    堆對象:new String(“123”),1個;

    共2個。

  • s2:

    字符串常量池對象:“1”、“2”、“12”,3個;

    堆對象:new String(“12”),1個

    共4個。

  • s3:

    字符串常量池對象:“12”、“3”、“123”,3個,

    堆對象:new String(“12”),1個;

    共4個。

總結:

new String()是在堆內存創建新的字符串對象,其構造參數中可傳入字符串,此字符串一般會在常量池中先創建出來,new String()創建的字符串是參數字符串的副本,看下API中關於String構造器的解釋:

所以new String()的方式創建字符串百分百會產生一個新的字符串對象,而類似於"123"這樣的字符串對象則需要在創建之前看常量池中有沒有,有的話就不創建,沒有則創建新的對象。 "+"操作符連接字符串常量的時候會在編譯器直接生成連接後的字符串,若該字符串在常量池已經存在,則不會創建新的字符串;連接變量的話則涉及StringBuilder等字符串構建器的創建,會在堆內存生成新的字符串對象。


如果覺得有幫助,希望老鐵們來個三連擊,給更多的人看到這篇文章

1、關注我的原創微信公衆號「武哥聊編程」,專注於Java、數據結構和算法、微服務、中間件等技術分享,保證你看完有所收穫。

2、給俺點個贊唄,可以讓更多的人看到這篇文章,順便激勵下我繼續寫作,嘻嘻。

作者info

【作者】:武哥
【公衆號】:武哥聊編程。歡迎大家關注~
【作者簡介】:同濟大學,碩士。先後在華爲、科大訊飛、拼多多采坑。一個自學 Java 的菜鳥,期待你的關注。

點贊是對我最大的鼓勵
↓↓↓↓↓↓

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章