Java 源碼剖析(01)--String 的特點是什麼?

1)String 有哪些重要的方法

以主流的 JDK 版本 1.8 來說,String 內部實際存儲結構爲 char 數組,源碼如下:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    // 用於存儲字符串的值
    private final char value[];
    // 緩存字符串的 hash code
    private int hash; // Default to 0
    // ......其他內容
}

String 源碼中包含下面幾個重要的方法:

1.1)多構造方法

String 字符串有以下 4 個重要的構造方法:

// String 爲參數的構造方法
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
// char[] 爲參數構造方法
public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}
// StringBuffer 爲參數的構造方法
public String(StringBuffer buffer) {
    synchronized(buffer) {
        this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
    }
}
// StringBuilder 爲參數的構造方法
public String(StringBuilder builder) {
    this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

其中,比較容易被我們忽略的是以 StringBuffer 和 StringBuilder 爲參數的構造函數,因爲這三種數據類型,我們通常都是單獨使用的,所以這個小細節我們需要特別留意一下。

1.2)equals() 比較兩個字符串是否相等

源碼如下:

public boolean equals(Object anObject) {
    // 對象引用相同直接返回 true
    if (this == anObject) {
        return true;
    }
    // 判斷需要對比的值是否爲 String 類型,如果不是則直接返回 false
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            // 把兩個字符串都轉換爲 char 數組對比
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // 循環比對兩個字符串的每一個字符
            while (n-- != 0) {
                // 如果其中有一個字符不相等就 true false,否則繼續對比
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

String 類型重寫了 Object 中的 equals() 方法,equals() 方法需要傳遞一個 Object 類型的參數值,在比較時會先通過 instanceof 判斷是否爲 String 類型,如果不是則會直接返回 false,instanceof 的使用如下:

Object oString = "123";
Object oInt = 123;
System.out.println(oString instanceof String); // 返回 true
System.out.println(oInt instanceof String); // 返回 false

當判斷參數爲 String 類型之後,會循環對比兩個字符串中的每一個字符,當所有字符都相等時返回 true,否則則返回 false。還有一個和 equals() 比較類似的方法 equalsIgnoreCase(),它是用於忽略字符串的大小寫之後進行字符串對比。

1.3) compareTo() 比較兩個字符串

compareTo() 方法用於比較兩個字符串,返回的結果爲 int 類型的值,源碼如下:

public int compareTo(String anotherString) {
    int len1 = value.length;
    int len2 = anotherString.value.length;
    // 獲取到兩個字符串長度最短的那個 int 值
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;
    int k = 0;
    // 對比每一個字符
    while (k < lim) {
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) {
            // 有字符不相等就返回差值
            return c1 - c2;
        }
        k++;
    }
    return len1 - len2;
}

從源碼中可以看出,compareTo() 方法會循環對比所有的字符,**當兩個字符串中有任意一個字符不相同時,則 return char1-char2。**比如,兩個字符串分別存儲的是 1 和 2,返回的值是 -1;如果存儲的是 1 和 1,則返回的值是 0 ,如果存儲的是 2 和 1,則返回的值是 1。還有一個和 compareTo() 比較類似的方法 compareToIgnoreCase(),用於忽略大小寫後比較兩個字符串。
可以看出 compareTo() 方法和 equals() 方法都是用於比較兩個字符串的,但它們有兩點不同:

  • equals() 可以接收一個 Object 類型的參數,而 compareTo() 只能接收一個 String 類型的參數;
  • equals() 返回值爲 Boolean,而 compareTo() 的返回值則爲 int。

它們都可以用於兩個字符串的比較,當 equals() 方法返回 true 時,或者是 compareTo() 方法返回 0 時,則表示兩個字符串完全相同。

1.4) 其他重要方法

  • indexOf():查詢字符串首次出現的下標位置 ;
  • lastIndexOf():查詢字符串最後出現的下標位置;
  • contains():查詢字符串中是否包含另一個字符串;
  • toLowerCase():把字符串全部轉換成小寫;
  • toUpperCase():把字符串全部轉換成大寫;
  • length():查詢字符串的長度;
  • trim():去掉字符串首尾空格;
  • replace():替換字符串中的某些字符;
  • split():把字符串分割並返回字符串數組;
  • join():把字符串數組轉爲字符串

2)知識擴展

2.1)== 和 equals 的區別

== 對於基本數據類型來說,是用於比較 “值”是否相等的;而對於引用類型來說,是用於比較引用地址是否相同的

查看源碼我們可以知道 Object 中也有 equals() 方法,源碼如下:

public boolean equals(Object obj) {
    return (this == obj);
}

可以看出,Object 中的 equals() 方法其實就是 ==,而 String 重寫了 equals() 方法把它修改成比較兩個字符串的值是否相等。源碼如下:

public boolean equals(Object anObject) {
    // 對象引用相同直接返回 true
    if (this == anObject) {
        return true;
    }
    // 判斷需要對比的值是否爲 String 類型,如果不是則直接返回 false
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            // 把兩個字符串都轉換爲 char 數組對比
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // 循環比對兩個字符串的每一個字符
            while (n-- != 0) {
                // 如果其中有一個字符不相等就 true false,否則繼續對比
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

2.2)final 修飾的好處

從 String 類的源碼我們可以看出 String 是被 final 修飾的不可繼承類,源碼如下:

public final class String 
	implements java.io.Serializable, Comparable<String>, CharSequence { //...... }

Java 語言之父 James Gosling 的回答是,他會更傾向於使用 final,因爲它能夠緩存結果,當你在傳參時不需要考慮誰會修改它的值;如果是可變類的話,則有可能需要重新拷貝出來一個新值進行傳參,這樣在性能上就會有一定的損失。

James Gosling 還說迫使 String 類設計成不可變的另一個原因是安全,當你在調用其他方法時,比如調用一些系統級操作指令之前,可能會有一系列校驗,如果是可變類的話,可能在你校驗過後,它的內部的值又被改變了,這樣有可能會引起嚴重的系統崩潰問題,這是迫使 String 類設計成不可變類的一個重要原因。

總結來說,使用 final 修飾的第一個好處是安全;第二個好處是高效,以 JVM 中的字符串常量池來舉例,如下兩個變量:

String s1 = "java";
String s2 = "java";

只有字符串是不可變時,我們才能實現字符串常量池,字符串常量池可以爲我們緩存字符串,提高程序的運行效率,如下圖所示:
在這裏插入圖片描述
試想一下如果 String 是可變的,那當 s1 的值修改之後,s2 的值也跟着改變了,這樣就和我們預期的結果不相符了,因此也就沒有辦法實現字符串常量池的功能了。

2.3)String 和 StringBuilder、StringBuffer 的區別

因爲 String 類型是不可變的,所以在字符串拼接的時候如果使用 String 的話性能會很低,因此我們就需要使用另一個數據類型 StringBuffer,它提供了 append 和 insert 方法可用於字符串的拼接,它使用 synchronized 來保證線程安全,如下源碼所示:

@Override
public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
}

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

因爲它使用了 synchronized 來保證線程安全,所以性能不是很高,於是在 JDK 1.5 就有了 StringBuilder,它同樣提供了 append 和 insert 的拼接方法,但它沒有使用 synchronized 來修飾,因此在性能上要優於 StringBuffer,所以在非併發操作的環境下可使用 StringBuilder 來進行字符串拼接。

2.4)String 和 JVM

String 常見的創建方式有兩種,new String() 的方式和直接賦值的方式,直接賦值的方式會先去字符串常量池中查找是否已經有此值,如果有則把引用地址直接指向此值,否則會先在常量池中創建,然後再把引用指向此值;而 new String() 的方式一定會先在堆上創建一個字符串對象,然後再去常量池中查詢此字符串的值是否已經存在,如果不存在會先在常量池中創建此字符串,然後把引用的值指向此字符串,如下代碼所示:

String s1 = new String("Java");
String s2 = s1.intern();
String s3 = "Java";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true

它們在 JVM 存儲的位置,如下圖所示:
【JDK 1.7 之後把永生代換成的元空間,把字符串常量池從方法區移到了 Java 堆上】
在這裏插入圖片描述
——————————————————————————————————————————————
關注公衆號,回覆 【算法】,獲取高清算法書!
在這裏插入圖片描述
內容來源《拉勾教育–Java 源碼剖析 34 講》
原文如下:
在這裏插入圖片描述

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