死啃了String原始碼之後

chenweicool發表於2020-06-08

Java原始碼之String

說在前面:

為什麼看原始碼: 最好的學習的方式就是模仿,接下來才是創造。而原始碼就是我們最好的模仿物件,因為寫原始碼的人都不是一般的人,所以用心學習原始碼,也就可能變成牛逼的人。其次,看原始碼,是一項修練內功的重要方式,書看百遍其意自現,原始碼也是一樣,前提是你不要懼怕原始碼,要用心的看,看不懂了,不要懷疑自己的智商,回過頭來多看幾遍,我就是這樣做的,一遍有一遍的感受,等你那天看原始碼不由的驚歎一聲,這個程式碼寫得太好了,恭喜你,你已經離優秀不遠了。最後,看原始碼,能培養我們的程式設計思維,當然這個層次有點高了,需要時間積累,畢竟我離這個境界也有點遠。

今天就來談談java的string的原始碼實現,後續我也會寫javaSe的原始碼系列,歡迎圍觀和交流。

1.繼承關係

繼承三個介面的說明:

Comparable介面:

實現物件之間比較的介面,它的核心方法只有一個:

public int compareTo(T o);

CharSequence介面:

CharSequence是char值的可讀序列。 該介面提供對許多不同種類的char序列的統一隻讀訪問。CharSequence是一個介面,它只包括length(), charAt(int index), subSequence(int start, int end)這幾個API介面。除了String實現了CharSequence之外,StringBufferStringBuilder也實現了 CharSequence介面。

那麼String為什麼實現Charsequence這個介面呢。這裡就要涉及一個java的重要特性,也就是多型。看下面的程式碼

   public void charSetTest(CharSequence charSequence){
        System.out.println(charSequence+"實現了多型");
    }
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        StringTest strTest = new StringTest();
        strTest.charSetTest(new String("我是String"));
        strTest.charSetTest(new StringBuffer("我是StringBuffer"));
        strTest.charSetTest(new StringBuilder("我是StringBuilder"));
}

執行結果:

我是String實現了多型
我是StringBuffer實現了多型
我是StringBuilder實現了多型

繼承這個介面的原因就很明顯:

因為String物件是不可變的,StringBuffer和StringBuilder這兩個是可變的,所以我們在構造字串的過程中往往要用到StringBuffer和StringBuilder。如果那些方法定義String作為引數型別,那麼就沒法對它們用那些方法,先得轉化成String才能用。但StringBuffer和StringBuilder轉換為String再轉換過來很化時間的,用它們而不是直接用String的“加法”來構造新String本來就是為了省時間。

Serializable介面:

繼承該介面,就是表明這個類是是可以別序列化的,這裡就標記了string這個類是可以被序列化的,序列化的定義以及使用時機可以移步這裡

2.常用方法的使用和原始碼解析:

2.1構造方法

string總共提供了15種構造方法:

當然,這麼多的構造方法,只需搞明白四個構造方法就可以了,因為其他的構造方法就是這四個構造方法的呼叫:

在看構造方法之前,先看看String的屬性

 private final char value[];
 private int hash; // Default to 0
 private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

通過屬性瞭解到,底層宣告瞭一個final型別的char陣列,這個char陣列就是String的構造方法的核心,因為String的本質就是字元char(只針對javaSE8 以前的版本),下面來分析有代表的四個構造方法:

  1. String()和String(String original)

    這兩個構造方法我們平時用的比較經常,原始碼如下:

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

    分析:

    通過原始碼可以看到,核心還是和String類宣告的char[]陣列value屬性建立聯絡,進而來處理這個屬性的值。

    在String()這個空構造的方法中,就是屬性char陣列的值宣告為一個空的字串賦值給屬性value.

    而在String(String original),也是將方法引數的value的屬性賦值給宣告char[]陣列的value屬性,方便String的其他方法對char[]陣列處理。

    記住,在java的String操作中,大多數情況下還是對char[]陣列的操作,這點很重要。

    一般情況下定義一個新的字串,有下面的兩種方式:

    String chen = new String("chen");   // 這個我們一般不會使用
    String chen = "chen";
    

    我們一般會選擇第二種,這又是什麼原因呢:

    其實這兩種宣告的方式在JVM看來時等價的。

    劃重點:

    但是String password="chen",利用了字串緩衝池,也就是說如果緩衝池中已經存在了相同的字串,就不會產生新的物件,而直接返回緩衝池中的字串物件的引用。

    如:
    String a = "chen";
    String b = "chen";
    String c = new String("chen");
    String d = new String("chen");
    
    System.out.println(a==b);//將輸出"true";因為兩個變數指向同一個物件。利用了字串的緩衝池
    System.out.println(c==d);//將輸出"flase";因為兩個變數不指向同一個物件。雖然值相同,只有用c.equals(d)才能返回true.
    

    所以實際中,建議用第一種,可以減少系統資源消耗。

  2. String(char[]vlaue,int offest,int count) 與字元相關的構造方法

    原始碼如下:

    public String(char value[], int offset, int count) {
            if (offset < 0) {
                throw new StringIndexOutOfBoundsException(offset);
            }
            if (count <= 0) {
                if (count < 0) {
                    throw new StringIndexOutOfBoundsException(count);
                }
                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);
            }
            this.value = Arrays.copyOfRange(value, offset, offset+count);
        }
    

    分析:

    這個構造方法的作用就是傳入指定大小的char[] 陣列,指定一個起始位置,然後構造出從起始位置開始計算的指定長度個數的字串,具體用法:

    public static void main(String[] args) {
            char[] chars = new char[]{'a','b','c','d'};
            
           // 從chars陣列的第二位開始,總數為3個字元的字串 
            String rangeChar=new String(chars,1,3);
            System.out.println(rangeChar);
        }
    
    輸出:
        abc   
    

    這個構造方法的核心在於:

    this.value = Arrays.copyOfRange(value, offset, offset+count);
    
    //而這個方法在追一層,就到了Arrays這個工具類copyOfRange這個方法
        public static char[] copyOfRange(char[] original, int from, int to) {
            int newLength = to - from;
            if (newLength < 0)
                throw new IllegalArgumentException(from + " > " + to);
            char[] copy = new char[newLength];
            System.arraycopy(original, from, copy, 0,
                             Math.min(original.length - from, newLength));
            return copy;
        }
    

    其實看到這裡,他的實現原理就基本清楚了,分析copyOfRange()這個方法的執行步驟:

    首先是獲取原字元陣列的orginal[]要構造字串的長度,也就是 這一行程式碼:

    int newLength = to - from;
    

    然後異常判斷,並宣告新的陣列,來儲存原陣列指定長度的值

     if (newLength < 0)
                throw new IllegalArgumentException(from + " > " + to);
            char[] copy = new char[newLength];
    

    將原字元陣列指定長度的值拷貝到新陣列,返回這個陣列:

     System.arraycopy(original, from, copy, 0,
                             Math.min(original.length - from, newLength));
            return copy;
    

    最後再將陣列的值賦值給String的屬性value,完成初始化:

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

    歸根結底還是和String宣告的屬性value建立聯絡,完成相關的操作。

  3. String(byte[] bytes,int offest,int length,Charset 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);
        }
    
    // StringCoding中的方法:
     static char[] decode(Charset cs, byte[] ba, int off, int len) {
             // 1, 構造解碼器
            CharsetDecoder cd = cs.newDecoder();
            int en = scale(len, cd.maxCharsPerByte());
            char[] ca = new char[en];
            if (len == 0)
                return ca;
            boolean isTrusted = false;
            if (System.getSecurityManager() != null) {
                if (!(isTrusted = (cs.getClass().getClassLoader0() == null))) {
                    ba =  Arrays.copyOfRange(ba, off, off + len);
                    off = 0;
                }
            }
            cd.onMalformedInput(CodingErrorAction.REPLACE)
              .onUnmappableCharacter(CodingErrorAction.REPLACE)
              .reset();
            if (cd instanceof ArrayDecoder) {
                int clen = ((ArrayDecoder)cd).decode(ba, off, len, ca);
                return safeTrim(ca, clen, cs, isTrusted);
            } else {
                ByteBuffer bb = ByteBuffer.wrap(ba, off, len);
                CharBuffer cb = CharBuffer.wrap(ca);
                try {
                    CoderResult cr = cd.decode(bb, cb, true);
                    if (!cr.isUnderflow())
                        cr.throwException();
                    cr = cd.flush(cb);
                    if (!cr.isUnderflow())
                        cr.throwException();
                } catch (CharacterCodingException x) {
                    // Substitution is always enabled,
                    // so this shouldn't happen
                    throw new Error(x);
                }
                return safeTrim(ca, cb.position(), cs, isTrusted);
            }
        }
    
     private static char[] safeTrim(char[] ca, int len,
                                       Charset cs, boolean isTrusted) {
            if (len == ca.length && (isTrusted || System.getSecurityManager() == null))
                return ca;
            else
                return Arrays.copyOf(ca, len);
        }
    

    這個方法構造的方法的複雜之處就是在於對於指定編碼的處理,但是我們如果看完這個方法呼叫的整個流程最終還是落到

    return Arrays.copyOf(ca, len);
    返回一個指定編碼的字元陣列,然後和String類的value屬性建立聯絡
    

    位元組陣列構造方法的基本邏輯:就是將位元組陣列轉化為字元陣列,再和String的value屬性建立聯絡,完成初始化。

  4. String(StringBuilder builder) 和String(StringBuffer buffer)與String擴充套件類相關的構造方法:

    這個構造方法就是將StringBuilder或者StringBuffer類初始化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());
        }
    

    分析:

    核心的程式碼還是這一句:

      this.value = Arrays.copyOf(builder.getValue(), builder.length());
    

​ 在往下看builder.getValue(),的原始碼

   final char[] getValue() {
        return value;
    }
返回一個字元陣列

這樣就能很好理解這個構造方法了: 先利用builder.getValue()將指定的型別轉化為字元陣列,通過Arrays.copyOf()方法進行拷貝,將返回的陣列賦值給String的屬性value,完成初始化。

2.2常用的方法分析

String的方法大概有60多個,這裡只分析幾個常用的方法,瞭解其他的方法,可以移步javaSE官方文件:

  1. 字串轉化為字元的方法:charAt(int index)

     public char charAt(int index) {
             //1. 判斷異常
            if ((index < 0) || (index >= value.length)) {
                throw new StringIndexOutOfBoundsException(index);
            }
            // 2.返回指定位置的字元
            return value[index];
        }
    

    用法示例:

            String StrChar = "chen";
            char getChar = StrChar.charAt(1);
            System.out.println(getChar);
    輸出:
        h
    

    2.字串轉化為位元組陣列的方法:getBytes()

    // 原始碼
    public byte[] getBytes() {
        return StringCoding.encode(value, 0, value.length);
    }
    
    // encode的原始碼
    static byte[] encode(char[] ca, int off, int len) {
            String csn = Charset.defaultCharset().name();
            try {
                // use charset name encode() variant which provides caching.
                return encode(csn, ca, off, len);
            } catch (UnsupportedEncodingException x) {
                warnUnsupportedCharset(csn);
            }
            try {
                return encode("ISO-8859-1", ca, 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;
            }
        }
    

​ 用法示例:

       String strByte = "chen";
       
       // 將string轉化為位元組陣列
       byte[] getBytes = strByte.getBytes();
       
       // 遍歷輸出
        for (byte getByte : getBytes) {
            System.out.println(getByte);
        }
 輸出結果:
     99
     104
     101
     110

3.返回字串中指定字元的下標的方法: indexOf(String str):

這裡的引數為字串:

這個方法一共涉及了四個方法,原始碼如下:

 public int indexOf(String str) {
        return indexOf(str, 0);
    }

 public int indexOf(String str, int fromIndex) {
        return indexOf(value, 0, value.length,
                str.value, 0, str.value.length, fromIndex);
    }

 static int indexOf(char[] source, int sourceOffset, int sourceCount,
            String target, int fromIndex) {
        return indexOf(source, sourceOffset, sourceCount,
                       target.value, 0, target.value.length,
                       fromIndex);
    }

static int indexOf(char[] source, int sourceOffset, int sourceCount,
            char[] target, int targetOffset, int targetCount,
            int fromIndex) {
    
        // 1. 判斷範圍
        if (fromIndex >= sourceCount) {
            return (targetCount == 0 ? sourceCount : -1);
        }
        if (fromIndex < 0) {
            fromIndex = 0;
        }
        if (targetCount == 0) {
            return fromIndex;
        }
       
        // 2,判斷目標字串是否時原子符串的子序列,並返回目標序列的第一個字元在原字元序列的索引。
        char first = target[targetOffset];
        int max = sourceOffset + (sourceCount - targetCount);

        for (int i = sourceOffset + fromIndex; i <= max; i++) {
            /* Look for first character. */
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }

            /* Found first character, now look at the rest of v2 */
            if (i <= max) {
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = targetOffset + 1; j < end && source[j]
                        == target[k]; j++, k++);

                if (j == end) {
                    /* Found whole string. */
                    return i - sourceOffset;
                }
            }
        }
        return -1;
    }

具體執行過程已在方法的註釋中進行了說明:

用法示例:

 String strIndex = "chen";
         int getIndex = strIndex.indexOf("he");
        System.out.println(getIndex);
輸出:
    1

注意:也就是輸出字串中的第一個字元在原子符串中的索引,前提是傳入的引數必須是原子符串的子序列,以上面的列子為例,傳入的字串序列必須是chen這個字串的子序列,才能輸出正確的索引,比如傳入的序列不是chen的子序列,輸出為-1

 String strIndex = "chen";
         int getIndex = strIndex.indexOf("hew");
        System.out.println(getIndex);
輸出:
    -1        

使用這個方法時,這點非常注意。

4.將字串中的某個字元進行替換:replace(char oldChar, char newChar)

引數就是被替換的字元和新的字元,原始碼如下:

public String replace(char oldChar, char newChar) {
    
    if (oldChar != newChar) {
        int len = value.length;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */
        
        // 1.遍歷字元陣列,找到原子符的位置
        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        //2. 宣告一個臨時的字元陣列,用來儲存替換後的字串,
        if (i < len) {
            char buf[] = new char[len];
            for (int j = 0; j < i; j++) {
                // 3. 將原字元陣列拷貝到新的字元陣列中去
                buf[j] = val[j];
            }
            while (i < len) {
                char c = val[i];
                // 4.
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            }
            // 3. 初始化一個新的字串
            return new String(buf, true);
        }
    }
    return this;
}

具體的執行邏輯就是註釋的語句。

用法示例:

 String strIndex = "chee";
        String afterReplace = strIndex.replace('e','n');
        System.out.println(afterReplace);
輸出:
    chnn

注意:這裡的替換是字串中的所有與舊字元的相同的字元,比如上面的這個例子,就是將原子符中的e全部替換為n。

5.字串的分隔:split(String regex) :

原始碼如下:

   public String[] split(String regex) {
        return split(regex, 0);
    }

  public String[] split(String regex, int limit) {
        /* fastpath if the regex is a
         (1)one-char String and this character is not one of the
            RegEx's meta characters ".$|()[{^?*+\\", or
         (2)two-char String and the first char is the backslash and
            the second is not the ascii digit or ascii letter.
         */
        char ch = 0;
        if (((regex.value.length == 1 &&
             ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 &&
              regex.charAt(0) == '\\' &&
              (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
              ((ch-'a')|('z'-ch)) < 0 &&
              ((ch-'A')|('Z'-ch)) < 0)) &&
            (ch < Character.MIN_HIGH_SURROGATE ||
             ch > Character.MAX_LOW_SURROGATE))
        {
            int off = 0;
            int next = 0;
            boolean limited = limit > 0;
            ArrayList<String> list = new ArrayList<>();
            while ((next = indexOf(ch, off)) != -1) {
                if (!limited || list.size() < limit - 1) {
                    list.add(substring(off, next));
                    off = next + 1;
                } else {    // last one
                    //assert (list.size() == limit - 1);
                    list.add(substring(off, value.length));
                    off = value.length;
                    break;
                }
            }
            // If no match was found, return this
            if (off == 0)
                return new String[]{this};

            // Add remaining segment
            if (!limited || list.size() < limit)
                list.add(substring(off, value.length));

            // Construct result
            int resultSize = list.size();
            if (limit == 0) {
                while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                    resultSize--;
                }
            }
            String[] result = new String[resultSize];
            return list.subList(0, resultSize).toArray(result);
        }
        return Pattern.compile(regex).split(this, limit);
    }

用法示例:

// 將一句話使用空格進行分隔 
String sentence = "People who hear thumb up can get rich";
        
       // 使用空格進行分隔
       String[] subSequence = sentence.split("\\s");
        for (String s : subSequence) {
            System.out.println(s);
    }

輸出:
    People
    who
    hear
    thumb
    up
    can
    get
    rich

注意: 使用這個方法,對正規表示式有所瞭解,才能實現更強大的功能,正規表示式的學習,可以移步菜鳥教程

6.實現字串的指定範圍的切割substring(int beginIndex, int endIndex):

原始碼如下:

 public String substring(int beginIndex, int endIndex) {
     
        // 1.判斷異常
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        // 2,確定切割的長度
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        // 3.使用構造方法,返回切割後字串
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

具體的執行邏輯如註釋所示,這個方法的邏輯總體比較簡單:

具體用法:

     String sentence = "hhchennn";
       String subSequence = sentence.substring(2,6);
        System.out.println(subSequence);
輸出:
    chen

注意: 從原始碼瞭解到,這個方法的在切割的時候,一般將第一個引數包含,包含第二個引數,也就是說上面的例子中在切割後的字串中包含2這個字元,但是不包含6這個字元。

7.當然處理這些還有一些常用的方法,比如:

// 1.去除字串前後空格的方法
trim()
    
//2.大小寫轉換的方法
 toLowerCase()
 toUpperCase()
    
//3. 將字串轉化為陣列
 toCharArray()
    
// 4.將基本型別轉化字串    
 valueOf(boolean b)
    
// 5.返回物件本身的字串形式   
 toString()

這些方法使用起來都比較簡單,強烈建議看看java官方文件

3.面試常問

3.1. 為什麼是不可變的

1、什麼是不可變?

從 java角度來講就是說成final的。參考Effective Java 中第 15 條 使可變性最小化 中對 不可變類 的解釋:

不可變類只是其例項不能被修改的類。每個例項中包含的所有資訊都必須在建立該例項的時候就提供,並且在物件的整個生命週期內固定不變。為了使類不可變,要遵循下面五條規則:

1. 不要提供任何會修改物件狀態的方法。

2. 保證類不會被擴充套件。 一般的做法是讓這個類稱為 `final` 的,防止子類化,破壞該類的不可變行為。

3. 使所有的域都是 final 的。

4. 使所有的域都成為私有的。 防止客戶端獲得訪問被域引用的可變物件的許可權,並防止客戶端直接修改這些物件。

5. 確保對於任何可變性元件的互斥訪問。 如果類具有指向可變物件的域,則必須確保該類的客戶端無法獲得指向這些物件的引用。

當然在 Java 平臺類庫中,包含許多不可變類,例如 String , 基本型別的包裝類,BigInteger, BigDecimal 等等。綜上所述,不可變類具有一些顯著的通用特徵:類本身是 final 修飾的;所有的域幾乎都是私有 final 的;不會對外暴露可以修改物件屬性的方法。通過查閱 String 的原始碼,可以清晰的看到這些特徵。

2.為什麼不可變

String real = "chen"
 real = "Wei";

下圖就很好解釋了程式碼的執行過程:

執行第一行程式碼時,在堆上新建一個物件例項 chen , real是一個指向該例項的引用,引用包含的僅僅只是例項在堆上的記憶體地址而已。執行第二行程式碼時,僅僅只是改變了 real 這個引用的地址,指向了另一個例項 wei。所以,正如前面所說過的,不可變類只是其例項不能被修改的類。real 重新賦值僅僅只是改變了它的引用而已,並不會真正去改變它本來的記憶體地址上的值。這樣的好處也是顯而易見的,最簡單的當存在多個 String 的引用指向同一個記憶體地址時,改變其中一個引用的值並不會對其他引用的值造成影響。

那麼,String 是如何保持不可變性的呢?結合 Effective Java 中總結的五條原則,閱讀它的 原始碼 之後就一清二楚了。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    
      /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

String 類是 final 修飾的,滿足第二條原則:保證類不會被擴充套件。 分析一下它的幾個域:

  • private final char value[] : 可以看到 Java 還是使用位元組陣列來實現字串的,並且用 final 修飾,保證其不可變性。這就是為什麼 String 例項不可變的原因。
  • private int hash : String的雜湊值快取
  • private static final long serialVersionUID = -6849794470754667710L : String物件的 serialVersionUID
  • private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0] : 序列化時使用

其中最主要的域就是 value,代表了 String物件的值。由於使用了 private final 修飾,正常情況下外界沒有辦法去修改它的值的。正如第三條 使所有的域都是 final 的。 和第四條 使所有的域都成為私有的 所描述的。難道這樣一個 private 加上 final 就可以保證萬無一失了嗎?看下面程式碼示例:

    final char[] value = {'a', 'b', 'c'};
    value[2] = 'd';

這時候的 value 物件在記憶體中已經是 a b d 了。其實 final 修飾的僅僅只是 value 這個引用,你無法再將 value 指向其他記憶體地址,例如下面這段程式碼就是無法通過編譯的:

    final char[] value = {'a', 'b', 'c'};
    value = {'a', 'b', 'c', 'd'};

所以僅僅通過一個 final 是無法保證其值不變的,如果類本身提供方法修改例項值,那就沒有辦法保證不變性了。Effective Java 中的第一條原則 不要提供任何會修改物件狀態的方法 。String 類也很好的做到了這一點。在 String 中有許多對字串進行操作的函式,例如 substring concat replace replaceAll 等等,這些函式是否會修改類中的 value 域呢?我們看一下 concat() 函式的內部實現:

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

注意其中的每一步實現都不會對 value產生任何影響。首先使用 Arrays.copyOf() 方法來獲得 value 的拷貝,最後重新 new 一個String物件作為返回值。其他的方法和 contact 一樣,都採取類似的方法來保證不會對 value 造成變化。的的確確,String 類中並沒有提供任何可以改變其值的方法。相比 final 而言,這更能保障 String 不可變。

其中最主要的域就是 value,代表了 String物件的值。由於使用了 private final 修飾,正常情況下外界沒有辦法去修改它的值的。正如第三條 使所有的域都是 final 的。 和第四條 使所有的域都成為私有的 所描述的。難道這樣一個 private 加上 final 就可以保證萬無一失了嗎?看下面程式碼示例:

    final char[] value = {'a', 'b', 'c'};
    value[2] = 'd';

這時候的 value 物件在記憶體中已經是 a b d 了。其實 final 修飾的僅僅只是 value 這個引用,你無法再將 value 指向其他記憶體地址,例如下面這段程式碼就是無法通過編譯的:

    final char[] value = {'a', 'b', 'c'};
    value = {'a', 'b', 'c', 'd'};

所以僅僅通過一個 final 是無法保證其值不變的,如果類本身提供方法修改例項值,那就沒有辦法保證不變性了。Effective Java 中的第一條原則 不要提供任何會修改物件狀態的方法 。String 類也很好的做到了這一點。在 String 中有許多對字串進行操作的函式,例如 substring concat replace replaceAll 等等,這些函式是否會修改類中的 value 域呢?我們看一下 concat() 函式的內部實現:

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

注意其中的每一步實現都不會對 value產生任何影響。首先使用 Arrays.copyOf() 方法來獲得 value 的拷貝,最後重新 new 一個String物件作為返回值。其他的方法和 contact 一樣,都採取類似的方法來保證不會對 value 造成變化。的的確確,String 類中並沒有提供任何可以改變其值的方法。相比 final 而言,這更能保障 String 不可變。

3.不可變類的好處:

Effective Java 中總結了不可變類的特點。

  • 不可變類比較簡單。
  • 不可變物件本質上是執行緒安全的,它們不要求同步。不可變物件可以被自由地共享。
  • 不僅可以共享不可變物件,甚至可以共享它們的內部資訊。
  • 不可變物件為其他物件提供了大量的構建。
  • 不可變類真正唯一的缺點是,對於每個不同的值都需要一個單獨的物件。

3.2. 使用什麼方式可以改變String類的不可變性

當然使用反射,java的反射機制可以做到我們平常做不到的很多事情:

        String str = "chen";
        System.out.println(str);
        Field field = String.class.getDeclaredField("value");
        field.setAccessible(true);
        char[] value = (char[]) field.get(str);
        value[1] = 'a';
        System.out.println(str);

執行結果:
  chen
  caen  

3.3. String和stringBuffer和StringBuilder的區別

從以下三個方面來考慮他們之間的異同點:

1.可變和不可變性:

String: 字串常量,在修改時,不會改變自身的值,若修改,就會重新生成新的字串物件。

StringBuffer: 在修改時會改變物件本身,不會生成新的物件,使用場景:對字元經常改變的情況下,主要方法: append(),insert() 等。

2.執行緒是否安全

String: 定義之後不可改變,執行緒安全

String Buffer: 是執行緒安全的,但是執行效率比較低,適用於多執行緒下操作字串緩衝區的大量資料。

StringBuilder: 執行緒不安全的,適用於單執行緒下操作字串緩衝區的大量資料

3.共同點

StringBuilder和StringBuffer有共有的父類 AbstractStringBuilder(抽象類)。

StringBuilder,StringBuffer的方法都會呼叫AbstractStringBuilder中的公共方法,如: super().append()...

只是StringBuffer會在方法上加上synchronized關鍵字,進行同步。

4.優秀的工具包推薦

4.1guava

Guava工程包含了若干被Google的 Java專案廣泛依賴 的核心庫,例如:集合 [collections] 、快取 [caching] 、原生型別支援 [primitives support] 、併發庫 [concurrency libraries] 、通用註解 [common annotations] 、字串處理 [string processing] 、I/O 等等。 所有這些工具每天都在被Google的工程師應用在產品服務中。具體的中文參考文件,Guava中文參考文件

4.2 Hutool

Hutool是一個Java工具包,也只是一個工具包,它幫助我們簡化每一行程式碼,減少每一個方法,讓Java語言也可以“甜甜的”。Hutool最初是我專案中“util”包的一個整理,後來慢慢積累並加入更多非業務相關功能,並廣泛學習其它開源專案精髓,經過自己整理修改,最終形成豐富的開源工具集。Hutool參考文件

追本溯源,方能闊步前行

參考資料:

參考部落格:

https://juejin.im/post/59cef72b518825276f49fe40

https://www.cnblogs.com/ChrisMurphy/p/4760197.html

參考書籍: java官方文件 《深入理解JVM虛擬機器》

相關文章