String能夠說是在Java開發中必不可缺的一種類,String容易忽略的細節也不少,對String的瞭解程度也反映了一個Java程序員的基本功。下面就由一個面試題來引出對String的剖析。java
從源碼能夠看出,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;
// 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()); }
/**比較兩個字符串是否相等,返回值爲布爾類型*/ 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判斷數據類型是否一致,是則進行下一步將兩個字符串轉換成字符數組逐一判斷。最後再返回判斷結果。面試
/*比較兩個字符串是否相等,返回值爲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數組
compareTo()
和equals()
都能比較兩字符串,當equals()返回true,compareTo()返回0時,都表示兩字符串徹底相同。compareTo()
是boolean,equals()
是int。compareTo()
是Object,equals()
只能是String類型。indexOf()
:查詢字符串首次出現的下標位置lastIndexOf()
:查詢字符串最後出現的下標位置contains()
:查詢字符串中是否包含另外一個字符串toLowerCase()
:把字符串所有轉換成小寫toUpperCase()
:把字符串所有轉換成大寫length()
:查詢字符串的長度trim()
:去掉字符串首尾空格replace()
:替換字符串中的某些字符split()
:把字符串分割並返回字符串數組join()
:把字符串數組轉爲字符串知道了String的實現和方法,下面就要引出常見的String面試問題緩存
Java 語言之父 James Gosling 的回答是,他會更傾向於使用 final,由於它可以緩存結果,當你在傳參時不須要考慮誰會修改它的值;若是是可變類的話,則有可能須要從新拷貝出來一個新值進行傳參,這樣在性能上就會有必定的損失。安全
James Gosling 還說迫使 String 類設計成不可變的另外一個緣由是安全,當你在調用其餘方法時,好比調用一些系統級操做指令以前,可能會有一系列校驗,若是是可變類的話,可能在你校驗事後,它的內部的值又被改變了,這樣有可能會引發嚴重的系統崩潰問題,這是迫使 String 類設計成不可變類的一個重要緣由。併發
因此只有當字符串不可改變時,才能利用字符常量池,保證在使用字符的時候不會被修改。app
那麼問題來了,咱們在使用final修飾一個變量時,不變的是引用地址,引用地址對應的對象是能夠發生變化的。如:ide
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] = 'b';//修改arr[2]的值 /**修改arr數組的地址,這裏會發生編譯錯誤,因此沒法修改引用地址 arr = new char[]{'1', '2', '3'};*/ System.out.println("arr的地址2:" + arr); System.out.println("arr的值2:" + Arrays.toString(arr)); } } /*運行結果: arr的地址1:[C@15db9742 arr的值1:[a b c d] arr的地址2:[C@15db9742 arr的值2:[a b b d] 顯然不變的是引用地址,引用地址所指對象的內容能夠被修改 */
而在上面1中的源碼裏,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]='b';//修改arr[2]的值 System.out.println("arr的地址2:" + arr); System.out.println("str= "+str); System.out.println("arr[]= "+Arrays.toString(arr)); } } /*運行結果: arr的地址1:[C@15db9742 str= abcd arr[]= [a, b, c, d] arr的地址2:[C@15db9742 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了一份,保證字符串不會被修改傳回。
==在基本類型中比較其對應的值,在引用類型中比較其地址值
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的引用地址必然不相同。
調用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
關於這三者的區別,主要借鑑這篇博文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.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()來處理。