String 原始碼淺析(一)

張少林同學發表於2019-01-19

String 原始碼淺析(一)

前言

相信作為 JAVAER,平時編碼時使用最多的必然是 String 字串,而相信應該存在不少人對於 Stringapi 很熟悉了,但沒有看過其原始碼實現,其實我個人覺得對於 api 的使用,最開始的階段是看其官方文件,而隨著開發經驗的積累,應當嘗試去看原始碼實現,這對自身能力的提升是至關重要的。當你理解了原始碼之後,後面對於 api 的使用也會更加得心應手!

備註:以下記錄基於 jdk8 環境

String 只是一個類

String 其實只是一個類,我們大致可以從以下幾個角度依次剖析它:

  1. 類繼承關係
  2. 類成員變數
  3. 類構造方法
  4. 類成員方法
  5. 相關靜態方法

繼承關係

IDEA 自帶外掛匯出 String 的 UML 類圖如下:

String 原始碼淺析(一)

從圖中馬上可以看出,String 實現了介面 SerializableComparableCharSequence,簡單介紹一下這三個介面的作用:

  • Serializable :實現該介面的類將具備序列化的能力,該介面沒有任何實現,僅僅是一直標識作用。
  • Comparable:實現此介面的類具備比較大小的能力,比如實現此介面的物件的列表(和陣列)可以由 Collections 類的靜態方法 sort 進行自動排序。
  • CharSequence:字元序列統一的我介面。提供字元序列通用的操作方法,通常是一些只讀方法,許多字元相關的類都實現此介面,以達到對字元序列的操作,比如:StringStringBuffer 等。

String 類定義如下:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence{
        ...
    }
複製程式碼

final 修飾符可知, String 類是無法被繼承,不可變類。

類成員變數

這裡主要介紹最關鍵的一個成員變數 value[],其定義如下:

 /** The value is used for character storage. */
    private final char value[];
複製程式碼

String 是一個字串,由字元 char 所組成,因此實際上 String 內部其實就是一個字元陣列,用 value[] 表示,注意這裡的 value[] 是用 final 修飾的,表示該值是不允許修改的

類構造方法

String 有很多過載的構造方法,介紹如下:

  1. 空引數構造方法,初始化字串例項,預設為空字元,理論上不需要用到這個構造方法,實際上定義一個空字元 String = "" 就會初始化一個空字串的 String 物件,而此構造方法,也是把空字元的 value[] 拷貝一遍而已,原始碼實現如下:
      public String() {
         this.value = "".value;
     }
    複製程式碼
  2. 通過一個字串引數構造 String 物件,實際上 將形參的 valuehash 賦值給例項物件作為初始化,相當於深拷貝了一個形參String物件,原始碼如下:
      public String(String original) {
            this.value = original.value;
            this.hash = original.hash;
        }
    複製程式碼
  3. 通過字元陣列去構建一個新的String物件,這裡使用 Arrays.copyOf 方法拷貝字元陣列
     public String(char value[]) {
            this.value = Arrays.copyOf(value, value.length);
        }
    複製程式碼
  4. 在源字元陣列基礎上,通過偏移量(起始位置)和字元數量,擷取構建一個新的String物件。
    public String(char value[], int offset, int count) {
            //如果偏移量小於0,則丟擲越界異常
            if (offset < 0) {
                throw new StringIndexOutOfBoundsException(offset);
            }
            if (count <= 0) {
                //如果字元數量小於0,則丟擲越界異常
                if (count < 0) {
                    throw new StringIndexOutOfBoundsException(count);
                }
                //在擷取的字元數量為0的情況下,偏移量在字串長度範圍內,則返回空字元
                if (offset <= value.length) {
                    this.value = "".value;
                    return;
                }
            }
            // Note: offset or count might be near -1>>>1.
            //如果偏移量大於字元總長度-擷取的字元長度,則丟擲越界異常
            if (offset > value.length - count) {
                throw new StringIndexOutOfBoundsException(offset + count);
            }
            //使用Arrays.copyOfRange靜態方法,擷取一定範圍的字元陣列,從offset開始,長度為offset+count,賦值給當前例項的字元陣列
            this.value = Arrays.copyOfRange(value, offset, offset+count);
        }
    複製程式碼
  5. 在源整數陣列的基礎上,通過偏移量(起始位置)和字元數量,擷取構建一個新的String物件。這裡的整數陣列表示字元對應的ASCII整數值
        public String(int[] codePoints, int offset, int count) {
        //如果偏移量小於0,則丟擲越界異常
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            //如果字元數量小於0,則丟擲越界異常
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            //在擷取的字元數量為0的情況下,偏移量在字串長度範圍內,則返回空字元
            if (offset <= codePoints.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        如果偏移量大於字元總長度-擷取的字元長度,則丟擲越界異常
        //if (offset > codePoints.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
    
        final int end = offset + count;
    
        // 這段邏輯是計算出字元陣列的精確大小n,過濾掉一些不合法的int資料
        int n = count;
        for (int i = offset; i < end; i++) {
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))
                continue;
            else if (Character.isValidCodePoint(c))
                n++;
            else throw new IllegalArgumentException(Integer.toString(c));
        }
    
        // 按照上一步驟計算出來的陣列大小初始化陣列
        final char[] v = new char[n];
        
        //遍歷填充字元陣列
        for (int i = offset, j = 0; i < end; i++, j++) {
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))
                v[j] = (char)c;
            else
                Character.toSurrogates(c, v, j++);
        }
        //賦值給當前例項的字元陣列
        this.value = v;
    }
    複製程式碼
  6. 通過源位元組陣列,按照一定範圍,從offset開始擷取length個長度,初始化 String 例項,同時可以指定字元編碼。
    public String(byte bytes[], int offset, int length, String charsetName)
            throws UnsupportedEncodingException {
        //字元編碼引數為空,丟擲空指標異常
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        //靜態方法 檢查位元組陣列的索引是否越界
        checkBounds(bytes, offset, length);
        //使用 StringCoding.decode 將位元組陣列按照一定範圍解碼為字串,從offset開始擷取length個長度
        this.value = StringCoding.decode(charsetName, bytes, offset, length);
    }
    複製程式碼
  7. 與第6個構造相似,只是編碼引數過載為 Charset 型別
      public String(byte bytes[], int offset, int length, Charset charset) {
        if (charset == null)
            throw new NullPointerException("charset");
        checkBounds(bytes, offset, length);
        this.value =  StringCoding.decode(charset, bytes, offset, length);
    }
    複製程式碼
  8. 通過源位元組陣列,構造一個字串例項,同時指定字元編碼,具體實現其實是呼叫第6個構造器,起始位置為0,擷取長度為位元組陣列長度
     public String(byte bytes[], String charsetName)
            throws UnsupportedEncodingException {
        this(bytes, 0, bytes.length, charsetName);
    }
    複製程式碼
  9. 通過源位元組陣列,構造一個字串例項,同時指定字元編碼,具體實現其實是呼叫第7個構造器,起始位置為0,擷取長度為位元組陣列長度
     public String(byte bytes[], Charset charset) {
        this(bytes, 0, bytes.length, charset);
    }
    複製程式碼
  10. 通過源位元組陣列,按照一定範圍,從offset開始擷取length個長度,初始化 String 例項,與第六個構造器不同的是,使用系統預設字元編碼
     public String(byte bytes[], int offset, int length) {
        //檢查索引是否越界
        checkBounds(bytes, offset, length);
        //使用系統預設字元編碼解碼位元組陣列為字元陣列
        this.value = StringCoding.decode(bytes, offset, length);
    }
    複製程式碼
  11. 通過源位元組陣列,構造一個字串例項,使用系統預設編碼,具體實現其實是呼叫第10個構造器,起始位置為0,擷取長度為位元組陣列長度
    public String(byte bytes[]) {
        this(bytes, 0, bytes.length);
    }
    複製程式碼
  12. StringBuffer 構建成一個新的String,比較特別的就是這個方法有synchronized鎖 同一時間只允許一個執行緒對這個 buffer 構建成String物件,是執行緒安全的
     public String(StringBuffer buffer) {
        //對當前 StringBuffer 物件加同步鎖
        synchronized(buffer) {
            //拷貝 StringBuffer 字元陣列給當前例項的字元陣列
            this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
        }
    }
    複製程式碼
  13. StringBuilder 構建成一個新的String,與第12個構造器不同的是,此構造器不是執行緒安全的
     public String(StringBuilder builder) {
        this.value = Arrays.copyOf(builder.getValue(), builder.length());
    }
    複製程式碼

類成員方法

  • 獲取字串長度,實際上是獲取字元陣列長度

      public int length() {
        return value.length;
    }
    複製程式碼
  • 判斷字串是否為空,實際上是盼復字元陣列長度是否為0

    public boolean isEmpty() {
        return value.length == 0;
    }
    複製程式碼
  • 根據索引引數獲取字元

     public char charAt(int index) {
        //索引小於0或者索引大於字元陣列長度,則丟擲越界異常
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        //返回字元陣列指定位置字元
        return value[index];
    }
    複製程式碼
  • 根據索引引數獲取指定字元ASSIC碼(int型別)

      public int codePointAt(int index) {
        //索引小於0或者索引大於字元陣列長度,則丟擲越界異
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        //返回索引位置指定字元ASSIC碼(int型別)
        return Character.codePointAtImpl(value, index, value.length);
    }
    複製程式碼
  • 返回index位置元素的前一個元素的ASSIC碼(int型)

    public int codePointBefore(int index) {
        //獲得index前一個元素的索引位置
        int i = index - 1;
        //檢查索引是否越界
        if ((i < 0) || (i >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return Character.codePointBeforeImpl(value, index, 0);
    }
    複製程式碼
  • 方法返回的是程式碼點個數,是實際上的字元個數,功能類似於length(),對於正常的String來說,length方法和codePointCount沒有區別,都是返回字元個數。但當String是Unicode型別時則有區別了。例如:String str = “/uD835/uDD6B” (即使 'Z' ), length() = 2 ,codePointCount() = 1

     public int codePointCount(int beginIndex, int endIndex) {
        if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
            throw new IndexOutOfBoundsException();
        }
        return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex);
    }
    複製程式碼
  • 也是相對Unicode字符集而言的,從index索引位置算起,偏移codePointOffset個位置,返回偏移後的位置是多少,例如,index = 2 ,codePointOffset = 3 ,maybe返回 5

    public int offsetByCodePoints(int index, int codePointOffset) {
        if (index < 0 || index > value.length) {
            throw new IndexOutOfBoundsException();
        }
        return Character.offsetByCodePointsImpl(value, 0, value.length,
                index, codePointOffset);
    }
    
    複製程式碼
  • 這是一個不對外的方法,是給String內部呼叫的,因為它是沒有訪問修飾符的,只允許同一包下的類訪問 引數:dst[]是目標陣列,dstBegin是目標陣列的偏移量,既要複製過去的起始位置(從目標陣列的什麼位置覆蓋) 作用就是將String的字元陣列value整個複製到dst字元陣列中,在dst陣列的dstBegin位置開始拷貝

    void getChars(char dst[], int dstBegin) {
        System.arraycopy(value, 0, dst, dstBegin, value.length);
    }
    複製程式碼
  • 得到char字元陣列,原理是getChars() 方法將一個字串的字元複製到目標字元陣列中。 引數:srcBegin是原始字串的起始位置,srcEnd是原始字串要複製的字元末尾的後一個位置(既複製區域不包括srcEnd) dst[]是目標字元陣列,dstBegin是目標字元的複製偏移量,複製的字元從目標字元陣列的dstBegin位置開始覆蓋。

    public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }
    複製程式碼
  • 獲取字串的位元組陣列,按照指定字元編碼將字串解碼為位元組陣列

    public byte[] getBytes(String charsetName)
            throws UnsupportedEncodingException {
        if (charsetName == null) throw new NullPointerException();
        return StringCoding.encode(charsetName, value, 0, value.length);
    }
    複製程式碼
  • 獲取字串的位元組陣列,按照指定字元編碼將字串解碼為位元組陣列

    public byte[] getBytes(Charset charset) {
        if (charset == null) throw new NullPointerException();
        return StringCoding.encode(charset, value, 0, value.length);
    }
    複製程式碼
  • 獲取字串的位元組陣列,按照系統預設字元編碼將字串解碼為位元組陣列

     public byte[] getBytes() {
        return StringCoding.encode(value, 0, value.length);
    }
    複製程式碼

簡單的總結

  • String 被 修飾符 final 修飾,是無法被繼承的,不可變類
  • String 實現 Serializable 介面,可以被序列化
  • String 實現 Comparable 介面,可以用於比較大小
  • String 實現 CharSequence 介面,表示一直有序字元序列,實現了通用的字元序列方法
  • String 是一個字元序列,內部資料結構其實是一個字元陣列,所有的操作方法都是圍繞這個字元陣列的操作。
  • String 中頻繁使用到了 System 類的 arraycopy 方法,目的是拷貝字元陣列

最後

由於篇幅原因,String 第一篇總結先到這裡,後續部分將寫另外寫一遍記錄,會第一時間推送公眾號【張少林同學】,歡迎關注!

String 原始碼淺析(一)

相關文章