與String相關的一大堆東西---【整理】

一,先看源碼

------------------------------------------------------------------------------------------------------------------------

一、定義

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

從該類的聲明中我們可以看出String是final類型的,表示該類不能被繼承,同時該類實現了三個接口:java.io.Serializable、 Comparable<String>、 CharSequence

二、屬性

private final char value[];

這是一個字符數組,並且是final類型,他用於存儲字符串內容,從fianl這個關鍵字中我們可以看出,String的內容一旦被初始化了是不能被更改的。 雖然有這樣的例子: String s = “a”; s = “b” 但是,這並不是對s的修改,而是重新指向了新的字符串, 從這裏我們也能知道,String其實就是用char[]實現的。

private int hash;

緩存字符串的hash Code,默認值爲 0

private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

因爲String實現了Serializable接口,所以支持序列化和反序列化支持。Java的序列化機制是通過在運行時判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,如果相同就認爲是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常(InvalidCastException)。

三、構造方法

String類作爲一個java.lang包中比較常用的類,自然有很多重載的構造方法.在這裏介紹幾種典型的構造方法:

1.使用字符數組、字符串構造一個String

我們知道,其實String就是使用字符數組(char[])實現的。所以我們可以使用一個字符數組來創建一個String,那麼這裏值得注意的是,當我們使用字符數組創建String的時候,會用到Arrays.copyOf方法和Arrays.copyOfRange方法。這兩個方法是將原有的字符數組中的內容逐一的複製到String中的字符數組中。同樣,我們也可以用一個String類型的對象來初始化一個String。這裏將直接將源String中的valuehash兩個屬性直接賦值給目標String。因爲String一旦定義之後是不可以改變的,所以也就不用擔心改變源String的值會影響到目標String的值。

當然,在使用字符數組來創建一個新的String對象的時候,不僅可以使用整個字符數組,也可以使用字符數組的一部分,只要多傳入兩個參數int offsetint count就可以了。


2.使用字節數組構造一個String

在Java中,String實例中保存有一個char[]字符數組,char[]字符數組是以unicode碼來存儲的,String 和 char 爲內存形式,byte是網絡傳輸或存儲的序列化形式。所以在很多傳輸和存儲的過程中需要將byte[]數組和String進行相互轉化。所以,String提供了一系列重載的構造方法來將一個字符數組轉化成String,提到byte[]和String之間的相互轉換就不得不關注編碼問題。String(byte[] bytes, Charset charset)是指通過charset來解碼指定的byte數組,將其解碼成unicode的char[]數組,夠造成新的String。

這裏的bytes字節流是使用charset進行編碼的,想要將他轉換成unicode的char[]數組,而又保證不出現亂碼,那就要指定其解碼方式

同樣使用字節數組來構造String也有很多種形式,按照是否指定解碼方式分的話可以分爲兩種:

String(byte bytes[]) String(byte bytes[], int offset, int length)

String(byte bytes[], Charset charset)

String(byte bytes[], String charsetName)

String(byte bytes[], int offset, int length, Charset charset)

String(byte bytes[], int offset, int length, String charsetName)

如果我們在使用byte[]構造String的時候,使用的是下面這四種構造方法(帶有charsetName或者charset參數)的一種的話,那麼就會使用StringCoding.decode方法進行解碼,使用的解碼的字符集就是我們指定的charsetName或者charset。 我們在使用byte[]構造String的時候,如果沒有指明解碼使用的字符集的話,那麼StringCodingdecode方法首先調用系統的默認編碼格式,如果沒有指定編碼格式則默認使用ISO-8859-1編碼格式進行編碼操作。主要體現代碼如下:

  static char[] decode(byte[] ba, int off, int len) {
        String csn = Charset.defaultCharset().name();
        try {
            // use charset name decode() variant which provides caching.
            return decode(csn, ba, off, len);
        } catch (UnsupportedEncodingException x) {
            warnUnsupportedCharset(csn);
        }
        try {
            return decode("ISO-8859-1", ba, off, len);
        } catch (UnsupportedEncodingException x) {
            // If this code is hit during VM initialization, MessageUtils is
            // the only way we will be able to get any kind of error message.
            MessageUtils.err("ISO-8859-1 charset not available: "
                             + x.toString());
            // If we can not find ISO-8859-1 (a required encoding) then things
            // are seriously wrong with the installation.
            System.exit(1);
            return null;
        }
    }

3.使用StringBuffer和StringBuider構造一個String

作爲String的兩個“兄弟”,StringBuffer和StringBuider也可以被當做構造String的參數。

    public String(StringBuffer buffer) {
        synchronized(buffer) {
            this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
        }
    }

    public String(StringBuilder builder) {
        this.value = Arrays.copyOf(builder.getValue(), builder.length());
    }

當然,這兩個構造方法是很少用到的,至少我從來沒有使用過,因爲當我們有了StringBuffer或者StringBuilfer對象之後可以直接使用他們的toString方法來得到String。關於效率問題,Java的官方文檔有提到說使用StringBuilder的toString方法會更快一些,原因是StringBuffer的toString方法是synchronized的,在犧牲了效率的情況下保證了線程安全。

 public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
 }

this.value = Arrays.copyOfRange(value, offset, offset+count);

4.一個特殊的保護類型的構造方法

String除了提供了很多公有的供程序員使用的構造方法以外,還提供了一個保護類型的構造方法(Java 7),我們看一下他是怎麼樣的:

String(char[] value, boolean share) {
    // assert share : "unshared not supported";
    this.value = value;
}

從代碼中我們可以看出,該方法和 String(char[] value)有兩點區別,第一個,該方法多了一個參數: boolean share,其實這個參數在方法體中根本沒被使用,也給了註釋,目前不支持使用false,只使用true。那麼可以斷定,加入這個share的只是爲了區分於String(char[] value)方法,不加這個參數就沒辦法定義這個函數,只有參數不能才能進行重載。那麼,第二個區別就是具體的方法實現不同。我們前面提到過,String(char[] value)方法在創建String的時候會用到 會用到ArrayscopyOf方法將value中的內容逐一複製到String當中,而這個String(char[] value, boolean share)方法則是直接將value的引用賦值給String的value。那麼也就是說,這個方法構造出來的String和參數傳過來的char[] value共享同一個數組。 那麼,爲什麼Java會提供這樣一個方法呢? 首先,我們分析一下使用該構造函數的好處:

首先,性能好,這個很簡單,一個是直接給數組賦值(相當於直接將String的value的指針指向char[]數組),一個是逐一拷貝。當然是直接賦值快了。

其次,共享內部數組節約內存

但是,該方法之所以設置爲protected,是因爲一旦該方法設置爲公有,在外面可以訪問的話,那就破壞了字符串的不可變性。例如如下YY情形:

char[] arr = new char[] {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
String s = new String(0, arr.length, arr); // "hello world"
arr[0] = 'a'; // replace the first character with 'a'
System.out.println(s); // aello world

如果構造方法沒有對arr進行拷貝,那麼其他人就可以在字符串外部修改該數組,由於它們引用的是同一個數組,因此對arr的修改就相當於修改了字符串。

所以,從安全性角度考慮,他也是安全的。對於調用他的方法來說,由於無論是原字符串還是新字符串,其value數組本身都是String對象的私有屬性,從外部是無法訪問的,因此對兩個字符串來說都很安全。

在Java 7 之有很多String裏面的方法都使用這種“性能好的、節約內存的、安全”的構造函數。比如:substringreplaceconcatvalueOf等方法(實際上他們使用的是public String(char[], int, int)方法,原理和本方法相同,已經被本方法取代)。

但是在Java 7中,substring已經不再使用這種“優秀”的方法了,爲什麼呢? 雖然這種方法有很多優點,但是他有一個致命的缺點,對於sun公司的程序員來說是一個零容忍的bug,那就是他很有可能造成內存泄露。 看一個例子,假設一個方法從某個地方(文件、數據庫或網絡)取得了一個很長的字符串,然後對其進行解析並提取其中的一小段內容,這種情況經常發生在網頁抓取或進行日誌分析的時候。下面是示例代碼。

String aLongString = "...a very long string..."; 
String aPart = data.substring(20, 40);
return aPart;

在這裏aLongString只是臨時的,真正有用的是aPart,其長度只有20個字符,但是它的內部數組卻是從aLongString那裏共享的,因此雖然aLongString本身可以被回收,但它的內部數組卻不能(如下圖)。這就導致了內存泄漏。如果一個程序中這種情況經常發生有可能會導致嚴重的後果,如內存溢出,或性能下降。

2aqQFnf

新的實現雖然損失了性能,而且浪費了一些存儲空間,但卻保證了字符串的內部數組可以和字符串對象一起被回收,從而防止發生內存泄漏,因此新的substring比原來的更健壯。

額、、、扯了好遠,雖然substring方法已經爲了其魯棒性放棄使用這種share數組的方法,但是這種share數組的方法還是有一些其他方法在使用的,這是爲什麼呢?首先呢,這種方式構造對應有很多好處,其次呢,其他的方法不會將數組長度變短,也就不會有前面說的那種內存泄露的情況(內存泄露是指不用的內存沒有辦法被釋放,比如說concat方法和replace方法,他們不會導致元數組中有大量空間不被使用,因爲他們一個是拼接字符串,一個是替換字符串內容,不會將字符數組的長度變得很短!)。

四、其他方法

length() 返回字符串長度

isEmpty() 返回字符串是否爲空

charAt(int index) 返回字符串中第(index+1)個字符

char[] toCharArray() 轉化成字符數組

trim() 去掉兩端空格

toUpperCase() 轉化爲大寫

toLowerCase() 轉化爲小寫

String concat(String str) //拼接字符串

String replace(char oldChar, char newChar) //將字符串中的oldChar字符換成newChar字符

//以上兩個方法都使用了String(char[] value, boolean share);

boolean matches(String regex) //判斷字符串是否匹配給定的regex正則表達式

boolean contains(CharSequence s) //判斷字符串是否包含字符序列s

String[] split(String regex, int limit) 按照字符regex將字符串分成limit份。

String[] split(String regex)

String string = "h,o,l,l,i,s,c,h,u,a,n,g";
String[] splitAll = string.split(",");
String[] splitFive = string.split(",",5);
splitAll =  [h, o, l, l, i, s, c, h, u, a, n, g]  
splitFive =  [h, o, l, l, i,s,c,h,u,a,n,g]

getBytes

在創建String的時候,可以使用byte[]數組,將一個字節數組轉換成字符串,同樣,我們可以將一個字符串轉換成字節數組,那麼String提供了很多重載的getBytes方法。但是,值得注意的是,在使用這些方法的時候一定要注意編碼問題。比如:

String s = "你好,世界!"; 
byte[] bytes = s.getBytes();

這段代碼在不同的平臺上運行得到結果是不一樣的。由於我們沒有指定編碼方式,所以在該方法對字符串進行編碼的時候就會使用系統的默認編碼方式,比如在中文操作系統中可能會使用GBK或者GB2312進行編碼,在英文操作系統中有可能使用iso-8859-1進行編碼。這樣寫出來的代碼就和機器環境有很強的關聯性了,所以,爲了避免不必要的麻煩,我們要指定編碼方式。如使用以下方式:

String s = "你好,世界!"; 
byte[] bytes = s.getBytes("utf-8");

比較方法

boolean equals(Object anObject);
boolean contentEquals(StringBuffer sb);
boolean contentEquals(CharSequence cs);
boolean equalsIgnoreCase(String anotherString);
int compareTo(String anotherString);
int compareToIgnoreCase(String str);
boolean regionMatches(int toffset, String other, int ooffset,int len)  //局部匹配
boolean regionMatches(boolean ignoreCase, int toffset,String other, int ooffset, int len)   //局部匹配

字符串有一系列方法用於比較兩個字符串的關係。 前四個返回boolean的方法很容易理解,前三個比較就是比較String和要比較的目標對象的字符數組的內容,一樣就返回true,不一樣就返回false,核心代碼如下:

 int n = value.length;
 while (n-- != 0) {
     if (v1[i] != v2[i])
         return false;
     i++;
 }

v1 v2分別代表String的字符數組和目標對象的字符數組。 第四個和前三個唯一的區別就是他會將兩個字符數組的內容都使用toUpperCase方法轉換成大寫再進行比較,以此來忽略大小寫進行比較。相同則返回true,不想同則返回false

在這裏,看到這幾個比較的方法代碼,有很多編程的技巧我們應該學習。我們看equals方法:

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            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;
    }

該方法首先判斷this == anObject ?,也就是說判斷要比較的對象和當前對象是不是同一個對象,如果是直接返回true,如不是再繼續比較,然後在判斷anObject是不是String類型的,如果不是,直接返回false,如果是再繼續比較,到了能終於比較字符數組的時候,他還是先比較了兩個數組的長度,不一樣直接返回false,一樣再逐一比較值。 雖然代碼寫的內容比較多,但是可以很大程度上提高比較的效率。值得學習~~!!!

contentEquals有兩個重載,StringBuffer需要考慮線程安全問題,再加鎖之後調用contentEquals((CharSequence) sb)方法。contentEquals((CharSequence) sb)則分兩種情況,一種是cs instanceof AbstractStringBuilder,另外一種是參數是String類型。具體比較方式幾乎和equals方法類似,先做“宏觀”比較,在做“微觀”比較。

下面這個是equalsIgnoreCase代碼的實現:

    public boolean equalsIgnoreCase(String anotherString) {
        return (this == anotherString) ? true
                : (anotherString != null)
                && (anotherString.value.length == value.length)
                && regionMatches(true, 0, anotherString, 0, value.length);
    }

看到這段代碼,眼前爲之一亮。使用一個三目運算符和&&操作代替了多個if語句。


hashCode

hashCode的實現其實就是使用數學公式:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

s[i]是string的第i個字符,n是String的長度。那爲什麼這裏用31,而不是其它數呢? 計算機的乘法涉及到移位計算。當一個數乘以2時,就直接拿該數左移一位即可!選擇31原因是因爲31是一個素數!

所謂素數:

質數又稱素數。指在一個大於1的自然數中,除了1和此整數自身外,沒法被其他自然數整除的數。

素數在使用的時候有一個作用就是如果我用一個數字來乘以這個素數,那麼最終的出來的結果只能被素數本身和被乘數還有1來整除!如:我們選擇素數3來做係數,那麼3*n只能被3和n或者1來整除,我們可以很容易的通過3n來計算出這個n來。這應該也是一個原因! (本段表述有問題,感謝 @沉淪 的提醒)

在存儲數據計算hash地址的時候,我們希望儘量減少有同樣的hash地址,所謂“衝突”。如果使用相同hash地址的數據過多,那麼這些數據所組成的hash鏈就更長,從而降低了查詢效率!所以在選擇係數的時候要選擇儘量長的係數並且讓乘法儘量不要溢出的係數,因爲如果計算出來的hash地址越大,所謂的“衝突”就越少,查找起來效率也會提高。

31可以 由i*31== (i<<5)-1來表示,現在很多虛擬機裏面都有做相關優化,使用31的原因可能是爲了更好的分配hash地址,並且31只佔用5bits!

在java乘法中如果數字相乘過大會導致溢出的問題,從而導致數據的丟失.

而31則是素數(質數)而且不是很長的數字,最終它被選擇爲相乘的係數的原因不過與此!

在Java中,整型數是32位的,也就是說最多有2^32= 4294967296個整數,將任意一個字符串,經過hashCode計算之後,得到的整數應該在這4294967296數之中。那麼,最多有 4294967297個不同的字符串作hashCode之後,肯定有兩個結果是一樣的, hashCode可以保證相同的字符串的hash值肯定相同,但是,hash值相同並不一定是value值就相同。


substring

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

前面我們介紹過,java 7 中的substring方法使用String(value, beginIndex, subLen)方法創建一個新的String並返回,這個方法會將原來的char[]中的值逐一複製到新的String中,兩個數組並不是共享的,雖然這樣做損失一些性能,但是有效地避免了內存泄露。


replaceFirst、replaceAll、replace區別

String replaceFirst(String regex, String replacement)
String replaceAll(String regex, String replacement)
String replace(CharSequence target, CharSequence replacement)

1)replace的參數是char和CharSequence,即可以支持字符的替換,也支持字符串的替換 2)replaceAll和replaceFirst的參數是regex,即基於規則表達式的替換,比如,可以通過replaceAll(“\d”, “*”)把一個字符串所有的數字字符都換成星號; 相同點是都是全部替換,即把源字符串中的某一字符或字符串全部換成指定的字符或字符串, 如果只想替換第一次出現的,可以使用 replaceFirst(),這個方法也是基於規則表達式的替換,但與replaceAll()不同的是,只替換第一次出現的字符串; 另外,如果replaceAll()和replaceFirst()所用的參數據不是基於規則表達式的,則與replace()替換字符串的效果是一樣的,即這兩者也支持字符串的操作;


copyValueOf 和 valueOf

String的底層是由char[]實現的:通過一個char[]類型的value屬性!早期的String構造器的實現呢,不會拷貝數組的,直接將參數的char[]數組作爲String的value屬性。然後test[0] = 'A';將導致字符串的變化。爲了避免這個問題,提供了copyValueOf方法,每次都拷貝成新的字符數組來構造新的String對象。但是現在的String對象,在構造器中就通過拷貝新數組實現了,所以這兩個方面在本質上已經沒區別了。

valueOf()有很多種形式的重載,包括:

  public static String valueOf(boolean b) {
      return b ? "true" : "false";
  }

  public static String valueOf(char c) {
       char data[] = {c};
       return new String(data, true);
  }
  public static String valueOf(int i) {
      return Integer.toString(i);
  }

  public static String valueOf(long l) {
     return Long.toString(l);
  }

 public static String valueOf(float f) {
     return Float.toString(f);
 }

 public static String valueOf(double d) {
    return Double.toString(d);
}

可以看到這些方法可以將六種基本數據類型的變量轉換成String類型。


intern()方法

public native String intern();

該方法返回一個字符串對象的內部化引用。 衆所周知:String類維護一個初始爲空的字符串的對象池,當intern方法被調用時,如果對象池中已經包含這一個相等的字符串對象則返回對象池中的實例,否則添加字符串到對象池並返回該字符串的引用。


String對“+”的重載

我們知道,Java是不支持重載運算符,String的“+”是java中唯一的一個重載運算符,那麼java使如何實現這個加號的呢?我們先看一段代碼:

public static void main(String[] args) {
    String string="hollis";
    String string2 = string + "chuang";
}

然後我們將這段代碼反編譯

public static void main(String args[]){
   String string = "hollis";
   String string2 = (new StringBuilder(String.valueOf(string))).append("chuang").toString();
}

看了反編譯之後的代碼我們發現,其實String對“+”的支持其實就是使用了StringBuilder以及他的append、toString兩個方法。


String.valueOf和Integer.toString的區別

接下來我們看以下這段代碼,我們有三種方式將一個int類型的變量變成呢過String類型,那麼他們有什麼區別?

1.int i = 5;
2.String i1 = "" + i;
3.String i2 = String.valueOf(i);
4.String i3 = Integer.toString(i);

1、第三行和第四行沒有任何區別,因爲String.valueOf(i)也是調用Integer.toString(i)來實現的。 2、第二行代碼其實是String i1 = (new StringBuilder()).append(i).toString();,首先創建一個StringBuilder對象,然後再調用append方法,再調用toString方法。

二,相關問題

------------------------------------------------------------------------------------------------------------------------

String,是Java中除了基本數據類型以外,最爲重要的一個類型了。很多人會認爲他比較簡單。但是和String有關的面試題有很多,下面我隨便找兩道面試題,看看你能不能都答對:

Q1:String s = new String("hollis");定義了幾個對象。(直面Java第025期

Q2:如何理解Stringintern方法?(直面Java第031期

上面這兩個是面試題和String相關的比較常考的,很多人一般都知道答案。

A1:若常量池中已經存在”hollis”,則直接引用,也就是此時只會創建一個對象,如果常量池中不存在”hollis”,則先創建後引用,也就是有兩個。

A2:當一個String實例str調用intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,如果有,則返回其的引用,如果沒有,則在常量池中增加一個Unicode等於str的字符串並返回它的引用;

兩個答案看上去沒有任何問題,但是,仔細想想好像哪裏不對呀。按照上面的兩個面試題的回答,就是說new String也會檢查常量池,如果有的話就直接引用,如果不存在就要在常量池創建一個,那麼還要intern幹啥?難道以下代碼是沒有意義的嗎?

String s = new String("Hollis").intern();

如果,每當我們使用new創建字符串的時候,都會到字符串池檢查,然後返回。那麼以下代碼也應該輸出結果都是true?

    String s1 = "Hollis";
    String s2 = new String("Hollis");
    String s3 = new String("Hollis").intern();

    System.out.println(s1 == s2);
    System.out.println(s1 == s3);

但是,以上代碼輸出結果爲(base jdk1.8.0_73):

false
true

不知道,聰明的讀者看完這段代碼之後,是不是有點被搞蒙了,到底是怎麼回事兒?

別急,且聽我慢慢道來。

字面量和運行時常量池

JVM爲了提高性能和減少內存開銷,在實例化字符串常量的時候進行了一些優化。爲了減少在JVM中創建的字符串的數量,字符串類維護了一個字符串常量池。

在JVM運行時區域的方法區中,有一塊區域是運行時常量池,主要用來存儲編譯期生成的各種字面量符號引用

瞭解Class文件結構或者做過Java代碼的反編譯的朋友可能都知道,在java代碼被javac編譯之後,文件結構中是包含一部分Constant pool的。比如以下代碼:

public static void main(String[] args) {
    String s = "Hollis";
}

經過編譯後,常量池內容如下:

 Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = String             #21            // Hollis
   #3 = Class              #22            // StringDemo
   #4 = Class              #23            // java/lang/Object
   ...
   #16 = Utf8               s
   ..
   #21 = Utf8               Hollis
   #22 = Utf8               StringDemo
   #23 = Utf8               java/lang/Object

上面的Class文件中的常量池中,比較重要的幾個內容:

   #16 = Utf8               s
   #21 = Utf8               Hollis
   #22 = Utf8               StringDemo

上面幾個常量中,s就是前面提到的符號引用,而Hollis就是前面提到的字面量。而Class文件中的常量池部分的內容,會在運行期被運行時常量池加載進去。關於字面量,詳情參考Java SE Specifications

new String創建了幾個對象

下面,我們可以來分析下String s = new String("Hollis");創建對象情況了。

這段代碼中,我們可以知道的是,在編譯期,符號引用s字面量Hollis會被加入到Class文件的常量池中,然後在類加載階段(具體時間段參考Java 中new String(“字面量”) 中 “字面量” 是何時進入字符串常量池的?),這兩個常量會進入常量池。

但是,這個“進入”階段,並不會直接把所有類中定義的常量全部都加載進來,而是會做個比較,如果需要加到字符串常量池中的字符串已經存在,那麼就不需要再把字符串字面量加載進來了。

所以,當我們說<若常量池中已經存在”hollis”,則直接引用,也就是此時只會創建一個對象>說的就是這個字符串字面量在字符串池中被創建的過程。

說完了編譯期的事兒了,該到運行期了,在運行期,new String("Hollis");執行到的時候,是要在Java堆中創建一個字符串對象的,而這個對象所對應的字符串字面量是保存在字符串常量池中的。但是,String s = new String("Hollis");對象的符號引用s是保存在Java虛擬機棧上的,他保存的是堆中剛剛創建出來的的字符串對象的引用。

所以,你也就知道以下代碼輸出結果爲false的原因了。

String s1 = new String("Hollis");
String s2 = new String("Hollis");
System.out.println(s1 == s2);

因爲,==比較的是s1s2在堆中創建的對象的地址,當然不同了。但是如果使用equals,那麼比較的就是字面量的內容了,那就會得到true

string

在不同版本的JDK中,Java堆和字符串常量池之間的關係也是不同的,這裏爲了方便表述,就畫成兩個獨立的物理區域了。具體情況請參考Java虛擬機規範。

所以,String s = new String("Hollis");創建幾個對象的答案你也就清楚了。

常量池中的“對象”是在編譯期就確定好了的,在類被加載的時候創建的,如果類加載時,該字符串常量在常量池中已經有了,那這一步就省略了。堆中的對象是在運行期才確定的,在代碼執行到new的時候創建的。

運行時常量池的動態擴展

編譯期生成的各種字面量符號引用是運行時常量池中比較重要的一部分來源,但是並不是全部。那麼還有一種情況,可以在運行期像運行時常量池中增加常量。那就是Stringintern方法。

當一個String實例調用intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,如果有,則返回其的引用,如果沒有,則在常量池中增加一個Unicode等於str的字符串並返回它的引用;

intern()有兩個作用,第一個是將字符串字面量放入常量池(如果池沒有的話),第二個是返回這個常量的引用。

我們再來看下開頭的那個讓人產生疑惑的例子:

    String s1 = "Hollis";
    String s2 = new String("Hollis");
    String s3 = new String("Hollis").intern();

    System.out.println(s1 == s2);
    System.out.println(s1 == s3);

你可以簡單的理解爲String s1 = "Hollis";String s3 = new String("Hollis").intern();做的事情是一樣的(但實際有些區別,這裏暫不展開)。都是定義一個字符串對象,然後將其字符串字面量保存在常量池中,並把這個字面量的引用返回給定義好的對象引用。

intern

對於String s3 = new String("Hollis").intern();,在不調用intern情況,s3指向的是JVM在堆中創建的那個對象的引用的(如圖中的s2)。但是當執行了intern方法時,s3將指向字符串常量池中的那個字符串常量。

由於s1和s3都是字符串常量池中的字面量的引用,所以s1==s3。但是,s2的引用是堆中的對象,所以s2!=s1。

intern的正確用法

不知道,你有沒有發現,在String s3 = new String("Hollis").intern();中,其實intern是多餘的?

因爲就算不用intern,Hollis作爲一個字面量也會被加載到Class文件的常量池,進而加入到運行時常量池中,爲啥還要多此一舉呢?到底什麼場景下才需要使用intern呢?

在解釋這個之前,我們先來看下以下代碼:

    String s1 = "Hollis";
    String s2 = "Chuang";
    String s3 = s1 + s2;
    String s4 = "Hollis" + "Chuang";

在經過反編譯後,得到代碼如下:

    String s1 = "Hollis";
    String s2 = "Chuang";
    String s3 = (new StringBuilder()).append(s1).append(s2).toString();
    String s4 = "HollisChuang";

可以發現,同樣是字符串拼接,s3和s4在經過編譯器編譯後的實現方式並不一樣。s3被轉化成StringBuilderappend,而s4被直接拼接成新的字符串。

如果你感興趣,你還能發現,String s3 = s1 + s2; 經過編譯之後,常量池中是有兩個字符串常量的分別是 HollisChuang(其實HollisChuangString s1 = "Hollis";String s2 = "Chuang";定義出來的),拼接結果HollisChuang並不在常量池中。

如果代碼只有String s4 = "Hollis" + "Chuang";,那麼常量池中將只有HollisChuang而沒有”Hollis” 和 “Chuang”。

究其原因,是因爲常量池要保存的是已確定的字面量值。也就是說,對於字符串的拼接,純字面量和字面量的拼接,會把拼接結果作爲常量保存到字符串池。

如果在字符串拼接中,有一個參數是非字面量,而是一個變量的話,整個拼接操作會被編譯成StringBuilder.append,這種情況編譯器是無法知道其確定值的。只有在運行期才能確定。

那麼,有了這個特性了,intern就有用武之地了。那就是很多時候,我們在程序中得到的字符串是只有在運行期才能確定的,在編譯期是無法確定的,那麼也就沒辦法在編譯期被加入到常量池中。

這時候,對於那種可能經常使用的字符串,使用intern進行定義,每次JVM運行到這段代碼的時候,就會直接把常量池中該字面值的引用返回,這樣就可以減少大量字符串對象的創建了。

如一深入解析String#intern文中舉的一個例子:

static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];

public static void main(String[] args) throws Exception {
    Integer[] DB_DATA = new Integer[10];
    Random random = new Random(10 * 10000);
    for (int i = 0; i < DB_DATA.length; i++) {
        DB_DATA[i] = random.nextInt();
    }
    long t = System.currentTimeMillis();
    for (int i = 0; i < MAX; i++) {
         arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
    }

    System.out.println((System.currentTimeMillis() - t) + "ms");
    System.gc();
}

在以上代碼中,我們明確的知道,會有很多重複的相同的字符串產生,但是這些字符串的值都是只有在運行期才能確定的。所以,只能我們通過intern顯示的將其加入常量池,這樣可以減少很多字符串的重複創建。

總結

我們再回到文章開頭那個疑惑:按照上面的兩個面試題的回答,就是說new String也會檢查常量池,如果有的話就直接引用,如果不存在就要在常量池創建一個,那麼還要intern幹啥?難道以下代碼是沒有意義的嗎?

String s = new String("Hollis").intern();

而intern中說的“如果有的話就直接返回其引用”,指的是會把字面量對象的引用直接返回給定義的對象。這個過程是不會在Java堆中再創建一個String對象的。

的確,以上代碼的寫法其實是使用intern是沒什麼意義的。因爲字面量Hollis會作爲編譯期常量被加載到運行時常量池。

之所以能有以上的疑惑,其實是對字符串常量池、字面量等概念沒有真正理解導致的。有些問題其實就是這樣,單個問題,自己都知道答案,但是多個問題綜合到一起就蒙了。歸根結底是知識的理解還停留在點上,沒有串成面。

 

 

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