Java String的相關性質分析

引言

String可以說是在Java開發中必不可缺的一種類,String容易忽略的細節也很多,對String的了解程度也反映了一個Java程序員的基本功。下面就由一個面試題來引出對String的剖析。

1. String在源碼里究竟是如何實現的,它有哪些方法,有什麼作用?

從源碼可以看出,String有三個私有方法,底層是由字符數組來存儲字符串

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /**存儲字符串的字符數組*/
    private final char value[];
    
    /** 緩存字符串的hashcode */
    private int hash; // 默認是0
    
    /** 用於驗證一致性來是否進行反序列化 */
    private static final long serialVersionUID = -6849794470754667710L;

1.1 String重要構造方法

// String 為參數的構造方法
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
// char[] 為參數構造方法
public String(char value[]) {
    //重新複製一份char數組的值和信息,保證字符串不會被修改傳回
    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());
}

1.2 String重要的方法

1.2.1 equals()方法

/**比較兩個字符串是否相等,返回值為布爾類型*/
public boolean equals(Object anObject) {//比較類型可以是object
    	/*引用對象相同時返回true*/
        if (this == anObject) {
            return true;
        }
    	/*判斷引用對象是否為String類型*/
        if (anObject instanceof String) { //instanceof用來判斷數據類型是否一致
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                //將兩個比較的字符串轉換成字符數組
                char v1[] = value;
                char v2[] = anotherString.value;
                //一個一個字符進行比較
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

equals()方法首先通過instanceof判斷數據類型是否一致,是則進行下一步將兩個字符串轉換成字符數組逐一判斷。最後再返回判斷結果。

1.2.2 compareTo()方法

/*比較兩個字符串是否相等,返回值為int類型*/
public int compareTo(String anotherString) {//比較類型只能是String類型
        int len1 = value.length;
        int len2 = anotherString.value.length;
    	/*獲得兩字符串最短的字符串長度lim*/
        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];
            //若兩字符不相等,返回c1-c2
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }

compareTo()通過逐一判斷兩字符串中的字符,不相等則返回兩字符差,反之循環結束最後返回0

小結
  1. compareTo()equals()都能比較兩字符串,當equals()返回true,compareTo()返回0時,都表示兩字符串完全相同。
  2. 同時兩者也有區別:
    • 返回類型compareTo()是boolean,equals()是int。
    • 字符類型compareTo()是Object,equals()只能是String類型。

1.3其他方法

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

知道了String的實現和方法,下面就要引出常見的String面試問題

2. String常見的面試問題

2.1 為什麼String類型要用final修飾?

  • 從上面的代碼可以看出,String類是被private final修飾的不可繼承類。那麼為何要用final修飾呢?

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

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

​ 所以只有當字符串不可改變時,才能利用字符常量池,保證在使用字符的時候不會被修改。

  • 那麼問題來了,我們在使用final修飾一個變量時,不變的是引用地址,引用地址對應的對象是可以發生變化的。如:

    import java.util.Arrays;
    public class IntTest{
    	public static void main(String args[]){
    		final char[] arr = new char[]{'a', 'b', 'c', 'd'};
    		System.out.println("arr的地址1:" + arr);
    		System.out.println("arr的值2:" + Arrays.toString(arr));
    		//修改arr[2]的值
            arr[2] = 'b';
            //修改arr數組的地址,這裡會發生編譯錯誤,所以無法修改引用地址
    		//arr = new char[]{'1', '2', '3'};
    		System.out.println("arr的地址2:" + arr);
    		System.out.println("arr的值2:" + Arrays.toString(arr));
    		
    	}
    }
    /*運行結果:
        arr的地址1:[[email protected]
        arr的值1:[a b c d]
        arr的地址2:[[email protected]
        arr的值2:[a b b d]
        
        顯然不變的是引用地址,引用地址所指對象的內容可以被修改
    */
    

    而在上述源碼中,String類下有一個私有的char數組成員

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence   {
        /**存儲字符串的字符數組*/
        private final char value[];
    

    那麼是否可以通過修改char數組所指對象的內容,來改變string的值呢?來試一試:

    import java.util.Arrays;
    public class IntTest{
    	public static void main(String args[]){
    		char[] arr = new char[]{'a','b','c','d'}; 		
    		String str = new String(arr);
        	System.out.println("arr的地址1:" + arr);
            System.out.println("str= " + str);
        	System.out.println("arr[]= "+Arrays.toString(arr));
            //修改arr[2]的值
    		arr[2]='b';	
        	System.out.println("arr的地址2:" + arr);
    		System.out.println("str= "+str);
    		System.out.println("arr[]= "+Arrays.toString(arr));
    		
    	}
    }
    /*運行結果:
        arr的地址1:[[email protected]
        str= abcd
        arr[]= [a, b, c, d]
        arr的地址2:[[email protected]
        str= abcd
        arr[]= [a, b, b, d]
    */
    

    顯然無法修改字符串,這是為何,我們再看看構造方法

    // String 為參數的構造方法
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    // char[] 為參數構造方法
    public String(char value[]) {
        //重新複製一份char數組的值和信息,保證字符串不會被修改傳回
        this.value = Arrays.copyOf(value, value.length);
    }
    

    發現string的構造方法里將原來的char數組的值和信息copy了一份,保證字符串不會被修改傳回。

2.2 equals()和 == 的區別

2.2.1 先說結論:

  • ==在基本類型中比較其對應的值,在引用類型中比較其地址值

  • equals()在未被重寫時和 == 完全一致,被重寫後是比較字符串的值

    public class StringTest {
        public static void main(String args[]) {
            String str1 = "Java"; //放在常量池中
            String str2 = new String("Java"); //在堆中創建對象str2的引用
            String str3 = str2; //指向堆中的str2的對象的引用
    		String str4 = "Java"; //從常量池中查找
            String str5 = new String("Java");
            System.out.println(str1 == str2); //false
            System.out.println(str1 == str3); //false
            System.out.println(str1 == str4); //true
    		System.out.println(str2 == str3); //true
            System.out.println(str2 == str5); //false
            System.out.println(str1.equals(str2)); //true
            System.out.println(str1.equals(str3)); //true
    		System.out.println(str1.equals(str4)); //true
            System.out.println(str2.equals(str3)); //true
        }
    }
    

    實際上equals()方法也是繼承Object的equals()方法。

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

    從上面的equals()方法的源碼可以看出,String在繼承方法後對應修改了方法中的相關內容,所以上述代碼的equals()方法輸出都是true。

    ​ 類似於String str1 = "Java"; 的和String str2 = new String("Java");形式有很大的區別,String str1 = "Java"; 形式首先在編譯過程中Java虛擬機就會去常量池中查找是否存在「Java」,如果存在,就會在棧內存中開闢一塊地方用於存儲其常量池中的地址。所以這種形式有可能創建了一個對象(常量池中),也可能一個對象也沒創建,即str1是直接在常量池中創建「Java」字符串,str4是先在常量池中查找有「Java」,所以直接地址直接指向常量池中已經存在的」Java「字符串。

    String str2 = new String("Java");的形式在編譯過程中,先去常量池中查找是否有「Java」,沒有則在常量池中新建”Java”。到了運行期,不管常量池中是否有「Java」,一律重新在堆中創建一個新的對象,然如果常量池中存在「Java」,複製一份放在堆中新開闢的空間中。如果不存在則會在常量池中創建一個「Java」後再複製到堆中。所以這種形式至少創建了一個對象,最多兩個對象。因此str1和str2的引用地址必然不相同。

2.3 string中的intern()方法

​ 調用intern方法時,如果常量池中存在該字符串,則返回池中的字符串。否則將此字符串對象添加到常量池中,並返回該字符串的引用。

String s1 = new String("Java");
String s2 = s1.intern();//直接指向常量池中的字符串
String s3 = "Java";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true

2.4 String和StringBuilder、StringBuffer的區別

​ 關於這三者的區別,主要借鑒這篇博文String,StringBuffer與StringBuilder的區別??首先,String是字符串常量,後兩者是字符串變量。其中StringBuffer是線程安全的,下面說說他們的具體區別。

​ String適用於字符串不可變的情況,因為在經常改變字符串的情形下,每次改變都會在堆內存中新建對象,會造成 JVM GC的工作負擔,因此在這種情形下,需要使用字符串變量。

​ 再說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;
}

​ 最後是StringBuilder,因為StringBuffer要保證線程安全,所以性能不是很高,於是在JDK1.5後引入了StringBuilder,在沒有了synchronize後性能得到提高,而且兩者的方法基本相同。所以在非並發操作下,如單線程情況可以使用StringBuilder來對字符串進行修改。

2.5 String中的「 + 」操作符

​ 其實在2.4中提到,String是字符串常量,具有不可變性。所以在拼接字符串、修改字符串時,盡量選擇StringBuilder和StringBuffer。下面再談一談String中出現「+」操作符的情況:

String s1 = "Ja";
String s2 = "va";
String s3 = "Java";
String s4 = "Ja" + "va"; //在編譯時期就在常量池中創建
String s5 = s1 + s2; //實際上s5是stringBuider,這個過程是stringBuilder的append

System.out.println("s3 == s4 " + (s3 == s4));
System.out.println("s3 == s5 " + (s3 == s5));

/**
運行結果:
	s3 == s4 true
	s3 == s5 false
*/

為什麼s4==s3結果是true? 反編譯看看:

1  String s = "Ja";//s1
2  String s1 = "va";//s2
3  String s2 = "Java";//s3
4  String s3 = "Java";//s4
5  String s4 = (new StringBuilder()).append(s).append(s1).toString();//s5
6  System.out.println((new StringBuilder()).append("s3 == s4").append(s2 == s3).toString());
7  System.out.println((new StringBuilder()).append("s3 == s5").append(s2 == s4).toString());

從第5行代碼中看出s4在編譯時期就已經將「Ja」+「va」編譯「Java」 ,這就是JVM的優化

第6行的代碼說明在s5 = s1 +s2;執行過程,s5變成StringBuilder,並利用append方法將s1和s2拼接。

因此在String類型中使用「+」操作符,編譯器一般會將其轉換成new StringBuilder().append()來處理。