java基礎:String — 源碼分析(一)

其餘更多java基礎文章:
java基礎學習(目錄)html


距離上次寫文章已經好一段時間了,主要是工做忙起來,看書的時間就少了,看String的進度就斷斷續續,在讀源碼的過程當中,我搜了幾篇頗有學習價值的文章,放在下面,能夠在閱讀完本文以後閱讀一下,有些地方我可能講的不夠清楚,下面文章裏的大神講的更仔細。java

學習資料:
String類API中文
深刻解析String#intern
Java 中new String("字面量") 中 "字面量" 是什麼時候進入字符串常量池的?
new一個String對象的時候,若是常量池沒有相應的字面量真的會去它那裏建立一個嗎?我表示懷疑。程序員

String的方法

String的底層是由char數組構成的正則表達式

private final char value[];

複製代碼

因爲底層char數組是final的,因此String對象是不可變的。數據庫

String的構造方法

咱們先講一下主要的幾種構造方法: 1. 參數爲String類型segmentfault

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

複製代碼

這裏將直接將源 String 中的 value 和 hash 兩個屬性直接賦值給目標 String。由於 String 一旦定義以後是不能夠改變的,因此也就不用擔憂改變源 String 的值會影響到目標 String 的值。數組

2. 參數爲字符數組安全

public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}
public String(char value[], int offset, int count)

複製代碼

這裏值得注意的是:當咱們使用字符數組建立 String 的時候,會用到 Arrays.copyOf 方法或 Arrays.copyOfRange 方法。這兩個方法是將原有的字符數組中的內容逐一的複製到 String 中的字符數組中。會建立一個新的字符串對象,隨後修改的字符數組不影響新建立的字符串。bash

3.參數爲字節數組
在 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 也有不少種形式,按照是否指定解碼方式分的話能夠分爲兩種:

public String(byte bytes[]){
  this(bytes, 0, bytes.length);
}
public String(byte bytes[], int offset, int length){
    checkBounds(bytes, offset, length);
    this.value = StringCoding.decode(bytes, offset, length);
}

複製代碼

若是咱們在使用 byte[] 構造 String 的時候,使用的是下面這四種構造方法(帶有 charsetName 或者 charset 參數)的一種的話,那麼就會使用 StringCoding.decode 方法進行解碼,使用的解碼的字符集就是咱們指定的 charsetName 或者 charset。

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 的時候,若是沒有指明解碼使用的字符集的話,那麼 StringCoding 的 decode 方法首先調用系統的默認編碼格式,若是沒有指定編碼格式則默認使用 ISO-8859-1 編碼格式進行編碼操做。主要體現代碼以下:

static char[] decode(byte[] ba, int off, int len){
    String csn = Charset.defaultCharset().name();
    try{ //use char set name decode() variant which provide scaching.
         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 initiali zation, MessageUtils is the only way we will be able to get any kind of error message.
       MessageUtils.err("ISO-8859-1 char set not available: " + x.toString());
       // If we can not find ISO-8859-1 (are quired encoding) then things are seriously wrong with the installation.
       System.exit(1);
       return null;
    }
}

複製代碼

4.參數爲StringBuilder或StringBuffer

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

4. 特殊的protected構造方法

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 的時候會用到 Arrays 的 copyOf 方法將 value 中的內容逐一複製到 String 當中,而這個 String(char[] value, boolean share) 方法則是直接將 value 的引用賦值給 String 的 value。那麼也就是說,這個方法構造出來的 String 和參數傳過來的 char[] value 共享同一個數組。

爲何 Java 會提供這樣一個方法呢?

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

  • 節約內存:該方法之因此設置爲 protected,是由於一旦該方法設置爲公有,在外面能夠訪問的話,若是構造方法沒有對 arr 進行拷貝,那麼其餘人就能夠在字符串外部修改該數組,因爲它們引用的是同一個數組,所以對 arr 的修改就至關於修改了字符串,那就破壞了字符串的不可變性。

  • 安全的:對於調用他的方法來講,因爲不管是原字符串仍是新字符串,其 value 數組自己都是 String 對象的私有屬性,從外部是沒法訪問的,所以對兩個字符串來講都很安全。

在 Java 7 以前有不少 String 裏面的方法都使用上面說的那種「性能好的、節約內存的、安全」的構造函數。 好比:substringreplaceconcatvalueOf等方法,實際上它們使用的是 public String(char[], ture) 方法來實現。

可是在 Java 7 中,substring 已經再也不使用這種「優秀」的方法了

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

複製代碼

爲何呢? 雖然這種方法有不少優勢,可是他有一個致命的缺點,對於 sun 公司的程序員來講是一個零容忍的 bug,那就是他頗有可能形成內存泄露。

看一個例子,假設一個方法從某個地方(文件、數據庫或網絡)取得了一個很長的字符串,而後對其進行解析並提取其中的一小段內容,這種狀況常常發生在網頁抓取或進行日誌分析的時候。

下面是示例代碼:

String aLongString = "...averylongstring...";
String aPart = data.substring(20, 40);
return aPart;

複製代碼

在這裏 aLongString 只是臨時的,真正有用的是 aPart,其長度只有 20 個字符,可是它的內部數組倒是從 aLongString 那裏共享的,所以雖然 aLongString 自己能夠被回收,但它的內部數組卻不能釋放。這就致使了內存泄漏。若是一個程序中這種狀況常常發生有可能會致使嚴重的後果,如內存溢出,或性能降低。

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

其餘方法

length() 返回字符串長度
isEmpty() 返回字符串是否爲空
charAt(int index) 返回字符串中第(index+1)個字符(數組索引)
char[] toCharArray() 轉化成字符數組
trim()去掉兩端空格
toUpperCase()轉化爲大寫
toLowerCase()轉化爲小寫
boolean matches(String regex) 判斷字符串是否匹配給定的regex正則表達式
boolean contains(CharSequence s) 判斷字符串是否包含字符序列 s
String[] split(String regex, int limit) 按照字符 regex將字符串分紅 limit 份
String[] split(String regex) 按照字符 regex 將字符串分段

複製代碼

詳細可查看String類API中文翻譯

須要注意

String concat(String str) 拼接字符串
String replace(char oldChar, char newChar) 將字符串中的
oldChar 字符換成 newChar 字符

複製代碼

以上兩個方法都使用了 String(char[] value, boolean share) concat 方法和 replace 方法,他們不會致使元數組中有大量空間不被使用,由於他們一個是拼接字符串,一個是替換字符串內容,不會將字符數組的長度變得很短,因此使用了共享的 char[] 字符數組來優化。

getBytes

在建立 String 的時候,可使用 byte[] 數組,將一個字節數組轉換成字符串,一樣,咱們能夠將一個字符串轉換成字節數組,那麼 String 提供了不少重載的 getBytes 方法。

public byte[] getBytes(){
  return StringCoding.encode(value, 0, value.length);
}

複製代碼

可是,值得注意的是,在使用這些方法的時候必定要注意編碼問題。好比: String s = "你好,世界!"; byte[] bytes = s.getBytes(); 這段代碼在不一樣的平臺上運行獲得結果是不同的。因爲沒有指定編碼方式,因此在該方法對字符串進行編碼的時候就會使用系統的默認編碼方式。

在中文操做系統中可能會使用 GBK 或者 GB2312 進行編碼,在英文操做系統中有可能使用 iso-8859-1 進行編碼。這樣寫出來的代碼就和機器環境有很強的關聯性了,爲了不沒必要要的麻煩,要指定編碼方式。

public byte[] getBytes(String charsetName) throws UnsupportedEncodingException{
  if (charsetName == null) throw new NullPointerException();
  return StringCoding.encode(charsetName, value, 0, value.length);
}

複製代碼

比較方法

boolean equals(Object anObject); 比較對象
boolean contentEquals(String Buffersb); 與字符串比較內容
boolean contentEquals(Char Sequencecs); 與字符比較內容
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;
}

複製代碼

經過源碼的代碼,咱們能夠了解它比較的流程:字符串相同:地址相同;地址不一樣,可是內容相同 這是一種提升效率的方法,也就是將比較快速的部分(地址,比較對象類型)放在前面比較,速度慢的部分(比較字符數組)放在後面執行。

StringBuffer 須要考慮線程安全問題,加鎖以後再調用

contentEquals()方法

public boolean contentEquals(CharSequence cs) {
        // Argument is a StringBuffer, StringBuilder
        if (cs instanceof AbstractStringBuilder) {
            if (cs instanceof StringBuffer) {
                synchronized(cs) {
                   return nonSyncContentEquals((AbstractStringBuilder)cs);
                }
            } else {
                return nonSyncContentEquals((AbstractStringBuilder)cs);
            }
        }
        // Argument is a String
        if (cs instanceof String) {
            return equals(cs);
        }
        // Argument is a generic CharSequence
        char v1[] = value;
        int n = v1.length;
        if (n != cs.length()) {
            return false;
        }
        for (int i = 0; i < n; i++) {
            if (v1[i] != cs.charAt(i)) {
                return false;
            }
        }
        return true;
    }

複製代碼

public boolean contentEquals(StringBuffer sb);實際調用了contentEquals(CharSequence cs)方法; AbstractStringBuilder和String都是接口CharSequence的實現,經過判斷輸入是AbstractStringBuilder仍是String的實例,執行不一樣的方法;

下面這個是 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()方法

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

複製代碼

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

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

爲何要使用這個公式,就是在存儲數據計算 hash 地址的時候,咱們但願儘可能減小有一樣的 hash 地址。若是使用相同 hash 地址的數據過多,那麼這些數據所組成的 hash 鏈就更長,從而下降了查詢效率。 因此在選擇係數的時候要選擇儘可能長的係數而且讓乘法儘可能不要溢出的係數,由於若是計算出來的 hash 地址越大,所謂的「衝突」就越少,查找起來效率也會提升。

選擇31做爲因子的緣由: 爲何 String hashCode 方法選擇數字31做爲乘子

substring
前面咱們介紹過,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(Char Sequencetarget, Char Sequencereplacement)

public String replace(char oldChar, char newChar){
  if(oldChar != newChar){
    int len = value.length;
    int i = -1;
    char[] val = value; /*avoid get field opcode*/
    while (++i < len){
      if (val[i] == oldChar){
        break;
      }
    }
    if( i < len ){
      char buf[] = new char[len];
      for (intj=0; j<i; j++){
        buf[j] = val[j];
      }
      while (i < len){
        char c = val[i];
        buf[i] = (c == oldChar) ? newChar : c;
        i++;
      }
      return new String(buf,true);
    }
   }
  return this;
}

複製代碼

replace 的參數能夠是 char 或者 CharSequence,便可以支持字符的替換, 也支持字符串的替換。當參數爲CharSequence時,實際調用的是replaceAll方法,因此replace方法是所有替換。 replaceAll 和 replaceFirst 的參數是 regex,即基於規則表達式的替換。區別是一個所有替換,一個只替換第一個。

intern()方法

public native String intern(); 

複製代碼

intern方法是Native調用,它的做用是在方法區中的常量池裏尋找等值的對象,若是沒有找到則在常量池中存放當前字符串的引用並返回該引用,不然直接返回常量池中已存在的String對象引用。

這個方法將會在下一章專門講
相關文章
相關標籤/搜索