Java字符串String那些事

2021-02-13 程式設計師專欄
引言

眾所周知在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-774673https://www.zhihu.com/question/36908414/answer/69724311https://www.cnblogs.com/paddix/p/5326863.htmlhttps://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html

相關焦點

  • Java –將字符串轉換為int
    : For input string: "10A" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:68) at java.base/java.lang.Integer.parseInt(Integer.java:658) at java.base/java.lang.Integer.valueOf
  • 完整的 string.Format對C#字符串格式化
    零佔位符:如果格式化的值在格式字符串中出現「0」的位置有一個數字,則此數字被複製到結果字符串中。小數點前最左邊的「0」的位置和小數點後最右邊的「0」的位置確定總在結果字符串中出現的數字範圍。「00」說明符使得值被捨入到小數點前最近的數字,其中零位總被捨去。數字佔位符:如果格式化的值在格式字符串中出現「#」的位置有一個數字,則此數字被複製到結果字符串中。
  • 關於Java字符串(String)10個最常見問題
    為什麼在安全敏感信息場合應該用char[]而不是string?String具有不可變的特性,當字符串一旦被創建,那麼知道垃圾收集器處理之前他們都是不可變的。如果使用數組,那麼你可以明確地改變其內部單元數據。因此,安全敏感的信息例如密碼不應該在系統中任何時候都存在。3. 我們可以在switch語句中使用string嗎?是的,在Java 7中可以!
  • JNI String類型
    JNI的字符操作方法Unicode編碼字符串jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);jsize GetStringLength(JNIEnv *env, jstring string);const jchar * GetStringChars(JNIEnv
  • Golang 語言怎麼高效使用字符串?
    如果需要修改,通常的做法是對原字符串進行截取和拼接操作,從而生成一個新字符串,但是會涉及內存分配和數據拷貝,從而有性能開銷。本文我們介紹在 Golang 語言中怎麼高效使用字符串。02字符串的數據結構在 Golang 語言中,字符串的值存儲在一塊連續的內存空間,我們可以把存儲數據的內存空間看作一個字節數組,字符串在 runtime 中的數據結構是一個結構體 stringStruct
  • 深入解析 String.intern
    「如果常量池中存在當前字符串, 就會直接返回當前字符串. 如果常量池中沒有此字符串, 會將此字符串放入常量池中後, 再返回」。2 native 代碼在 jdk7後,oracle 接管了 JAVA 的源碼後就不對外開放了,根據 jdk 的主要開發人員聲明 openJdk7 和 jdk7 使用的是同一分主代碼,只是分支代碼會有些許的變動。
  • Redis-簡單動態字符串(SDS)
    1.何為SDS  SDS(simple dynamic string)又名簡單動態字符串,是Redis
  • python 通關字符串操作方法詳解-大量案例
    Python實際三類字符串:  1.通常意義字符串(str)  2.原始字符串,以大寫R 或 小寫r開始,r'',不對特殊字符進行轉義  3.Unicode字符串,u'' basestring子類  python中字符串支持索引、切片操作。
  • 深入解析String中的intern
    「如果常量池中存在當前字符串, 就會直接返回當前字符串. 如果常量池中沒有此字符串, 會將此字符串放入常量池中後, 再返回」。2,native 代碼在 jdk7後,oracle 接管了 JAVA 的源碼後就不對外開放了,根據 jdk 的主要開發人員聲明 openJdk7 和 jdk7 使用的是同一分主代碼,只是分支代碼會有些許的變動。
  • Java String 對象,你真的了解了嗎?| CSDN 博文精選
    我們知道 char 是兩個字節,如果用來存一個字節的字符有點浪費,為了節約空間,Java 公司就改成了一個字節的byte來存儲字符串。這樣在存儲一個字節的字符是就避免了浪費。在 Java9 維護了一個新的屬性 coder,它是編碼格式的標識,在計算字符串長度或者調用 indexOf() 函數時,需要根據這個欄位,判斷如何計算字符串長度。
  • 為啥Java中的Switch不支持long?卻支持String呢?
    在沒有實驗之前,我想當然的認為它是不是根據枚舉的 int 型欄位來計算的(因為一般枚舉都是一個int型,一個string型),但是轉念一想,萬一枚舉沒有 int 型欄位呢,萬一有多個 int 型欄位呢,所以肯定不是這樣的,下面看實驗吧。
  • JS中字符串操作,史上最全匯總!
    // console.log("aaa");// console.log("aa");// console.log('name="lilei"');// console.log("i can't say sth");// console.log("i can't say sth"); //轉義// console.log("this string
  • 試試 StringTokenizer,性能可以快 4 倍!!
    其實在 JDK 中,還有一個性能很強的純字符串分割工具類:StringTokenizer。這個類在 JDK 1.0 中就推出來了,但在實際工作卻發現很少有人使用,網上有人說不建議使用了,甚至還有人說已經廢棄了,真的是這樣嗎?
  • Java 中的 Switch 是如何支持 String 的?為什麼不支持 long?
    在沒有實驗之前,我想當然的認為它是不是根據枚舉的 int 型欄位來計算的(因為一般枚舉都是一個int型,一個string型),但是轉念一想,萬一枚舉沒有 int 型欄位呢,萬一有多個 int 型欄位呢,所以肯定不是這樣的,下面看實驗吧。
  • 乾貨 Java 中的 String 為什麼是不可變的?
    在JDK1.6中,String的成員變量有以下幾個:public final class String implements java.io.Serializable, Comparable<String>, CharSequence{ /** The value is used for character storage
  • SO面試題09:如何將String轉換為Int?
    例如,給你一個字符串 「1234」,返回的應該是整型 1234。如下面這個實例:int foo;try { foo = Integer.parseInt(myString);}catch (NumberFormatException e){ foo = 0;}另外,您可以使用Guava庫中的Ints方法,該方法與Java 8的Optional結合使用,提供了一種強大而簡潔的方法來將字符串轉換為
  • Java中的 Switch 是如何支持 String 的?為什麼不支持 long?
    在沒有實驗之前,我想當然的認為它是不是根據枚舉的 int 型欄位來計算的(因為一般枚舉都是一個int型,一個string型),但是轉念一想,萬一枚舉沒有 int 型欄位呢,萬一有多個 int 型欄位呢,所以肯定不是這樣的,下面看實驗吧。
  • Java 中的 Switch 都支持 String 了,為什麼不支持 long?
    在沒有實驗之前,我想當然的認為它是不是根據枚舉的 int 型欄位來計算的(因為一般枚舉都是一個int型,一個string型),但是轉念一想,萬一枚舉沒有 int 型欄位呢,萬一有多個 int 型欄位呢,所以肯定不是這樣的,下面看實驗吧。
  • String 數據類型 轉 int類型
    接受通過以下語法給出的十進位、十六進位和八進位數字;parseInt(String s)或parseInt(String s, int radix)將字符串參數作為有符號的十進位整數進行解析。除了第一個字符可以是用來表示負值的 ASCII 減號 '-' ('\u002D') 外,字符串中的字符都必須是十進位數字。
  • C++STL(一)----string和vector
    例如: string str; string(const char* s); //使用字符串s初始化string(const string& str); //使用一個string對象初始化另一個string對象string(int n, char c); //使用n個字符c初始化示例:string