深刻研究Java String

開始寫 Java 一年來,一直都是遇到什麼問題再去解決,尚未主動的深刻的去學習過 Java 語言的特性和深刻閱讀 JDK 的源碼。既然決定從此靠 Java 吃飯,仍是得花些心思在上面,放棄一些打遊戲的時間,系統深刻的去學習。html

Java String 是 Java 編程中最經常使用的類之一,也是 JDK 提供的最基礎的類。因此我決定先從 String 類入手,深刻的研究一番來開個好頭。java

類定義與類成員

打開 JDK 中的 String 源碼,最早應當關注 String 類的定義。git

public final class String implements java.io.Serializable, Comparable<String>, CharSequence 複製代碼

不可繼承與不可變

寫過 Java 的人都知道, 當 final 關鍵字修飾類時,表明此類不可繼承。因此 String 類是不能被外部繼承。這時候咱們可能會好奇,String 的設計者 爲何要把它設計成不可繼承的呢。我在知乎上找到了相關的問題和討論, 我以爲首位的回答已經說的很明白了。String 作爲 Java 的最基礎的引用數據類型,最重要的一點就是不可變性,因此使用 final 就是爲了禁止繼承 破壞了 String 的不可變的性質github

實現類的不可變性,不光是用 final 修飾類這麼簡單,從源碼中能夠看到,String 其實是對一個字符數組的封裝,而字符數組是私有的,而且沒有提供 任何能夠修改字符數組的方法,因此一旦初始化完成, String 對象便沒法被修改。express

序列化

從上面的類定義中咱們看到了 String 實現了序列化的接口 Serializable,因此 String 是支持序列化和反序列化的。 什麼是Java對象的序列化?相信不少和我同樣的 Java 菜鳥都有這樣疑問。深刻分析Java的序列化與反序列化這篇文章中的這一段話 解釋的很好。編程

Java平臺容許咱們在內存中建立可複用的Java對象,但通常狀況下, 只有當JVM處於運行時,這些對象纔可能存在, 即,這些對象的生命週期不會比JVM的生命週期更長。但在現實應用中, 就可能要求在JVM中止運行以後可以保存(持久化)指定的對象,並在未來從新讀取被保存的對象。 Java對象序列化就可以幫助咱們實現該功能。 使用Java對象序列化,在保存對象時,會把其狀態保存爲一組字節,在將來,再將這些字節組裝成對象。 必須注意地是,對象序列化保存的是對象的」狀態」,即它的成員變量。由此可知,對象序列化不會關注類中的靜態變量。 除了在持久化對象時會用到對象序列化以外,當使用RMI(遠程方法調用),或在網絡中傳遞對象時,都會用到對象序列化。 Java序列化API爲處理對象序列化提供了一個標準機制,該API簡單易用。數組

在 String 源碼中,咱們也能夠看到支持序列化的類成員定義。網絡

/** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /** * Class String is special cased within the Serialization Stream Protocol. * * A String instance is written into an ObjectOutputStream according to * <a href="{@docRoot}/../platform/serialization/spec/output.html"> * Object Serialization Specification, Section 6.2, "Stream Elements"</a> */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];
複製代碼

serialVersionUID 是一個序列化版本號,Java 經過這個 UID 來斷定反序列化時的字節流與本地類的一致性,若是相同則認爲一致, 能夠進行反序列化,若是不一樣就會拋出異常。app

serialPersistentFields 這個定義則比上一個少見許多,大概猜到是與序列化時的類成員有關係。爲了弄懂這個字段的意義,我 google 百度齊上,也 僅僅只找到了 JDK 文檔對類 ObjectStreamField的一丁點描述, A description of a Serializable field from a Serializable class. An array of ObjectStreamFields is used to declare the Serializable fields of a class. 大意是這個類用來描述序列化類的一個序列化字段, 若是定義一個此類的數組則能夠聲明類須要被序列化的字段。可是仍是沒有找到這個類的具體用法和做用是怎樣的。後來我仔細看了一下這個字段的定義, 與 serialVersionUID 應該是一樣經過具體字段名來定義各類規則的,而後我直接搜索了關鍵字 serialPersistentFields,終於找到了它的具體做用。 即,默認序列化自定義包括關鍵字 transient 和靜態字段名 serialPersistentFieldstransient 用於指定哪一個字段不被默認序列化, serialPersistentFields 用於指定哪些字段須要被默認序列化。若是同時定義了 serialPersistentFieldstransienttransient 會被忽略。 我本身也測試了一下,確實是這個效果。性能

知道了 serialPersistentFields 的做用之後,問題又來了,既然這個靜態字段是用來定義參與序列化的類成員的,那爲何在 String 中這個數組的長度定義爲0? 通過一番搜索查找資料之後,仍是沒有找到一個明確的解釋,期待若是有大佬看到能解答一下。

可排序

String 類還實現了 Comparable 接口,Comparable<T>接口只有一個方法 public int compareTo(T o),實現了這個接口就意味着該類支持排序, 便可用 Collections.sortArrays.sort 等方法對該類的對象列表或數組進行排序。

在 String 中咱們還能夠看到這樣一個靜態變量,

public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator implements Comparator<String>, java.io.Serializable {
        // use serialVersionUID from JDK 1.2.2 for interoperability
        private static final long serialVersionUID = 8575799808933029326L;

        public int compare(String s1, String s2) {
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }

        /** Replaces the de-serialized object. */
        private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
    }
複製代碼

從上面的源碼中能夠看出,這個靜態成員是一個實現了 Comparator 接口的類的實例,而實現這個類的做用是比較兩個忽略大小寫的 String 的大小。

那麼 ComparableComparator 有什麼區別和聯繫呢?同時 String 又爲何要兩個都實現一遍呢?

第一個問題這裏就不展開了,總結一下就是,Comparable 是類的內部實現,一個類能且只能實現一次,而 Comparator 則是外部實現,能夠經過不改變 類自己的狀況下,爲類增長更多的排序功能。 因此咱們也能夠爲 String 實現一個 Comparator使用,具體能夠參考Comparable與Comparator的區別這篇文章。

String 實現了兩種比較方法的意圖,其實是一目瞭然的。實現 Comparable 接口爲類提供了標準的排序方案,同時爲了知足大多數排序需求的忽略大小寫排序的狀況, String 再提供一個 Comparator 到公共靜態類成員中。若是還有其餘的需求,那就只能咱們本身實現了。

類方法

String 的方法大體能夠分爲如下幾類。

  • 構造方法
  • 功能方法
  • 工廠方法
  • intern方法

關於 String 的方法的解析,這篇文章已經解析的夠好了,因此我這裏也再也不重複的說一遍了。不過 最後的 intern 方法值得咱們去研究。

intern方法

字符串常量池

String 作爲 Java 的基礎類型之一,可使用字面量的形式去建立對象,例如 String s = "hello"。固然也可使用 new 去建立 String 的對象, 可是幾乎不多看到這樣的寫法,長此以往我便習慣了第一種寫法,可是殊不知道背後大有學問。下面一段代碼能夠看出他們的區別。

public class StringConstPool {
    public static void main(String[] args) {
        String s1 = "hello world";
        String s2 = new String("hello world");
        String s3 = "hello world";
        String s4 = new String("hello world");
        String s5 = "hello " + "world";
        String s6 = "hel" + "lo world";
        String s7 = "hello";
        String s8 = s7 + " world";
        
        System.out.println("s1 == s2: " + String.valueOf(s1 == s2) );
        System.out.println("s1.equals(s2): " + String.valueOf(s1.equals(s2)));
        System.out.println("s1 == s3: " + String.valueOf(s1 == s3));
        System.out.println("s1.equals(s3): " + String.valueOf(s1.equals(s3)));
        System.out.println("s2 == s4: " + String.valueOf(s2 == s4));
        System.out.println("s2.equals(s4): " + String.valueOf(s2.equals(s4)));
        System.out.println("s5 == s6: " + String.valueOf(s5 == s6));
        System.out.println("s1 == s8: " + String.valueOf(s1 == s8));
    }
}
/* output s1 == s2: false s1.equals(s2): true s1 == s3: true s1.equals(s3): true s2 == s4: false s2.equls(s4): true s5 == s6: true s1 == s8: false */

複製代碼

從這段代碼的輸出能夠看到,equals 比較的結果都是 true,這是由於 String 的 equals 比較的值( Object 對象的默認 equals 實現是比較引用, String 對此方法進行了重寫)。== 比較的是兩個對象的引用,若是引用相同則返回 true,不然返回 falses1==s2: falses2==s4: false 說明了 new 一個對象必定會生成一個新的引用返回。s1==s3: true 則證實了使用字面量建立對象一樣的字面量會獲得一樣的引用。

s5 == s6 實際上和 s1 == s3 在 JVM 眼裏是同樣的狀況,由於早在編譯階段,這種常量的簡單運算就已經完成了。咱們可使用 javap 反編譯一下 class 文件去查看 編譯後的狀況。

➜ ~ javap -c StringConstPool.class
Compiled from "StringConstPool.java"
public class io.github.jshanet.thinkinginjava.constpool.StringConstPool {
  public io.github.jshanet.thinkinginjava.constpool.StringConstPool();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String hello world
       2: astore_1
       3: return
}
複製代碼

看不懂彙編也不要緊,由於註釋已經很清楚了......

s1 == s8 的狀況就略複雜,s8 是經過變量的運算而得,因此沒法在編譯時直接算出其值。而 Java 又不能重載運算符,因此咱們在 JDK 的源碼裏也 找不到相關的線索。萬事不絕反編譯,咱們再經過反編譯看看實際上編譯器對此是否有影響。

public class io.github.jshanet.thinkinginjava.constpool.StringConstPool {
  public io.github.jshanet.thinkinginjava.constpool.StringConstPool();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String hello
       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  world
      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
}
複製代碼

經過反編譯的結果能夠發現,String 的變量運算實際上在編譯後是由 StringBuilder 實現的,s8 = s7 + " world" 的代碼等價於 (new StringBuilder(s7)).append(" world").toString()Stringbuilder 是可變的類,經過 append 方法 和 toString 將兩個 String 對象聚合 成一個新的 String 對象,因此到這裏就不難理解爲何 s1 == s8 : false 了。

之因此會有以上的效果,是由於有字符串常量池的存在。字符串對象的分配和其餘對象同樣是要付出時間和空間代價,而字符串又是程序中最經常使用的對象,JVM 爲了提升性能和減小內存佔用,引入了字符串的常量池,在使用字面量建立對象時, JVM 首先會去檢查常量池,若是池中有現成的對象就直接返回它的引用,若是 沒有就建立一個對象,並放到池裏。由於字符串不可變的特性,因此 JVM 不用擔憂多個變量引用同一個對象會改變對象的狀態。同時運行時實例建立的全局 字符串常量池中有一個表,老是爲池中的每一個字符串對象維護一個引用,因此這些對象不會被 GC 。

intern 方法的做用

上面說了不少都沒有涉及到主題 intern 方法,那麼 intern 方法到做用究竟是什麼呢?首先查看一下源碼。

/** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * <p> * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java&trade; Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */
    public native String intern();
複製代碼

Oracle JDK 中,intern 方法被 native 關鍵字修飾而且沒有實現,這意味着這部分到實現是隱藏起來了。從註釋中看到,這個方法的做用是若是常量池 中存在當前字符串,就會直接返回當前字符串,若是常量池中沒有此字符串,會將此字符串放入常量池中後再返回。經過註釋的介紹已經能夠明白這個方法的做用了, 再用幾個例子證實一下。

public class StringConstPool {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = new String("hello");
        String s3 = s2.intern();
        System.out.println("s1 == s2: " + String.valueOf(s1 == s2));
        System.out.println("s1 == s3: " + String.valueOf(s1 == s3));
    }
}
/* output s1 == s2: false s1 == s3: true */
複製代碼

這裏就很容易的瞭解 intern 實際上就是把普通的字符串對象也關聯到常量池中。

固然 intern 的實現原理和最佳實踐等也是須要理解學習的,美團技術團隊的這篇深刻解析String#intern 很深刻也很詳細,推薦閱讀。

相關文章
相關標籤/搜索