1. 程式人生 > >Java 7 原始碼學習系列(一)——String

Java 7 原始碼學習系列(一)——String

String表示字串,Java中所有字串的字面值都是String類的例項,例如“ABC”。字串是常量,在定義之後不能被改變,字串緩衝區支援可變的字串。因為 String 物件是不可變的,所以可以共享它們。例如:

String str = "abc";

相當於

char data[] = {'a', 'b', 'c'};
String str = new String(data);

 

這裡還有一些其他使用字串的例子:

 System.out.println("abc");
 String cde = "cde";
 System.out.println("abc" + cde);
 String c = "abc".substring(2,3);
 String d = cde.substring(1, 2);

String類提供了檢查字元序列中單個字元的方法,比如有比較字串,搜尋字串,提取子字串,建立一個字串的副本、字串的大小寫轉換等。例項對映是基於Character類中指定的Unicode標準的。 Java語言提供了對字串連線運算子的特別支援(+),該符號也可用於將其他型別轉換成字串。字串的連線實際上是通過StringBuffer或者StringBuilderappend()方法來實現的,字串的轉換通過toString方法實現,該方法由 Object 類定義,並可被 Java 中的所有類繼承。 除非另有說明,傳遞一個空引數在這類建構函式或方法會導致NullPointerException

異常被丟擲。String表示一個字串通過UTF-16(unicode)格式,補充字元通過代理對(參見Character類的 Unicode Character Representations 獲取更多的資訊)表示。索引值參考字元編碼單元,所以補充字元在String中佔兩個位置。


定義 屬性 構造方法

使用字元陣列、字串構造一個String

使用位元組陣列構造一個String

使用StringBuffer和StringBuider構造一個String

一個特殊的保護型別的構造方法

其他方法

getBytes

比較方法

hashCode

substring

replaceFirst、replaceAll、replace區別

copyValueOf 和 valueOf

intern

String對“+”的過載

String.valueOf和Integer.toString的區別

參考資料


一、定義

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方法。


參考資料:

open jdk

Java™ Platform, Standard Edition 7 API Specification

Java7為什麼要修改substring的實現

關於hashcode 裡面 使用31 係數的問題

from: http://www.hollischuang.com/archives/99#String.valueOf%E5%92%8CInteger.toString%E7%9A%84%E5%8C%BA%E5%88%AB