Java字符串String那些事

Python實戰社羣

Java實戰社羣

長按識別下方二維碼,按需求添加

掃碼關注添加客服

進Python社羣▲

掃碼關注添加客服

進Java社羣

作者丨java金融

來源丨java金融(ID:java4299)

引言

衆所周知在java裏面除了8種基本數據類型的話,還有一種特殊的類型String,這個類型是我們每天搬磚都基本上要使用它。

String 類型可能是 Java 中應用最頻繁的引用類型,但它的性能問題卻常常被忽略。高效的使用字符串,可以提升系統的整體性能。當然,要做到高效使用字符串,需要深入瞭解其特性。

String類

我們可以看下String類的源碼:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

從源碼上我們是不是可以發現String類是被final關鍵字所修飾的,String類的數據是通過char[] 數組來存儲的。數組也是被final修飾的所以String 對象是不可被更改的。接下來我們再看看String的一些方法:像concat、replace、substring等都是返回了一個新的new String感興趣的可以去看看String的一些常見方法。當我們執行這些方法之後最原始的字符串是沒有改變的,都是返回新的字符串。

 public static void main(String[] args) {
        String str = new String("java金融");
        String str1 = str.substring(0, 4);
        String str2 = str.concat("公衆號");
        String str3 = str.replace("java金融", "關注:【java金融】");
        // 還有其他的方法
        System.out.println(str1);
        System.out.println(str2);
        System.out.println(str3);
        System.out.println(str);
    }

輸出結果

java
java金融公衆號
關注:【java金融】
java金融

所以我們只要記住一點:“String對象一旦被創建就是固定不變的了, 對String對象的任何改變都不影響到原對象,相關的任何change操作都會生成新的對象”。

字符串常量池

JVM中,爲了減少字符串對象的重複創建,維護了一塊特殊的內存空間,這塊內存就被稱爲全局字符串常量池(string pool也有叫做string literal pool)。

字符串常量池的位置

字符串常量池所在的位置也是跟不同的jdk版本有關係的。

  • JDK6及之前字符串常量池存放在方法區, 此時hotspot虛擬機對方法區的實現爲永久代。

  • JDK7字符串常量池被從方法區拿到了堆中, 這裏沒有提到運行時常量池,也就是說字符串常量池被單獨拿到堆,運行時常量池剩下的東西還在方法區, 也就是hotspot中的永久代。

  • JDK8 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字符串常量池還在堆裏只不過把方法區的實現從永久代變成了元空間(Metaspace) 。

String# intern

String::intern()是一個本地方法,它的作用是如果字符串常量池中已經包含一個等於此String對象的字符串,則返回代表池中這個字符串的String對象的引用;否則,會將此String對象包含的字符串添加到常量池中,並且返回此String對象的引用。

上述定義出自《深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)》我們知道了這個 String::intern()這個方法的作用下面來看幾道並沒有什麼用的題目看看你是否都能夠回答對?

        String str2 = new String("java") + new String("金融"); // 1
         str2.intern(); // 2
         String str1 = "java金融"; // 3
         System.out.println(str2 == str1);

這個代碼在JDK6中輸出結果是false,在jdk7輸出是true。爲何會因爲不同的jdk版本輸出結果不一樣,因爲不同版本字符串常量池的位置發生了變化。下面來分析下爲何會產生這種差異。字符串雖然不屬於基本數據類型但是它也可以想基本類型一樣,直接通過字面量來賦值,同時也是可以通過new 來生成字符串對象。通過字面量賦值的方式和new 的方式 生成字符串還是有區別的。

  • 字面量賦值:通過字面量賦值(使用雙引號聲明出來的String)會先去常量池中查找是否已經有相同的字符串,如果已經存在棧中的引用直接指向該字符串,如果不存在就在常量中生成一個字符串再將棧中的引用指向該字符串。

  • new 的方式創建:而通過new的方式創建字符串時,就直接在堆中生成一個字符串的對象棧中的引用指向該對象。對於堆中的字符串對象,可以通過 intern() 方法來將字符串添加的常量池中,並返回指向該常量的引用。jdk6 結果是false,是因爲常量池是在永久代的Perm區和java堆是兩個區域。所以兩個區域的對象地址比較是不同的。JDK7結果是true, 這個原因主要是從JDK 7及以後,HotSpot 將常量池從永久代移到了堆,正因爲如此,JDK7 及以後的intern方法在實現上發生了比較大的改變,JDK7及以後,intern方法還是會先去查詢常量池中是否有已經存在,如果存在,則返回常量池中的引用,這一點與之前沒有區別,區別在於如果在常量池找不到對應的字符串則不會再將字符串拷貝到常量池,而只是在常量池中生成一個對原字符串的引用。所以爲什麼返回true 是因爲執行完標號爲1的時候常量池中沒有"「java金融」"對象的,接下來標號爲2的時候 會在常量池生成一個“「java金融」”的對象會直接存一個對堆中“「java金融」”的引用,標號爲3:進行字面量賦值的時候常量池已經存在了所以直接返回該引用。所以都是指向堆中的字符串返回true「如果把3行代碼放到第一行上面結果又不一樣了,感興趣的可以動手試一試並且分析下原因哦。」

string 常見性能優化

使用+號拼接字符串

字符串拼接是我們平時在代碼中使用最頻繁的了。

  • +號拼接靜態字符串

   String str = "關注"+"公衆號:"+"java金融";

我們可以通過反編譯查看下上述代碼:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // String 關注公衆號:java金融
         2: astore_1
         3: return
      LineNumberTable:
        line 11: 0
        line 12: 3
}

我們可以發現編譯器直接幫我們優化了,直接生成了一個字符串“關注公衆號:java金融” 並沒有生成中間變量的String實例。如果我們上述代碼稍微變化下

   public static void main(String[] args) {
        String str ="關注";
        String str1 = str + "公衆號:java金融";
    }

 stack=2, locals=3, args_size=1
         0: ldc           #2                  // String 關注
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: aload_1
        11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: ldc           #6                  // String 公衆號:java金融
        16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: astore_2
        23: return
      LineNumberTable:
        line 11: 0
        line 12: 3
        line 13: 23

從反編譯代碼中我們會發現生成了StringBuilder對象來進行追加。

  • 所以String + 拼接變量的時候底層是通過StringBuilder來實現的,我們循環操作拼接字符串的時候也應當使用StringBuilder替代+,否則的話每一次循環都會創建 一個StringBuilder 對象。

  • 對於靜態字符串的拼接操作,Java在編譯時會進行徹底的優化,會把多個拼接字符串在編譯時合成一個單獨的長字符串。

常見字符串經典面試題

關於字符串最常見的面試題,面試寶典常見的題目。「String s = new String("xyz")」 創建了多少個實例?一般的回答都會是2個,(一個是“xyz”,一個是指向“xyz”的引用對象s) 答案並沒有那麼簡單哦,可以看看大佬的回答還是非常精彩的。連接地址https://www.iteye.com/blog/rednaxelafx-774673(文末第一個參考地址)

https://www.iteye.com/blog/rednaxelafx-774673 https://www.zhihu.com/question/36908414/answer/69724311 https://www.cnblogs.com/paddix/p/5326863.html https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html

程序員專欄 掃碼關注填加客服 長按識別下方二維碼進羣


近期精彩內容推薦:   再見!深圳!再見!騰訊! 瘋傳朋友圈的 Pony 馬化騰的講話 SpringBoot 實現併發登錄人數控制 異步 Python 比同步 Python 快在哪裏?



在看點這裏好文分享給更多人↓↓
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章