死啃了String源碼以後

Java源碼之String

說在前面:

爲何看源碼: 最好的學習的方式就是模仿,接下來纔是創造。而源碼就是咱們最好的模仿對象,由於寫源碼的人都不是通常的人,因此用心學習源碼,也就可能變成牛逼的人。其次,看源碼,是一項修練內功的重要方式,書看百遍其意自現,源碼也是同樣,前提是你不要害怕源碼,要用心的看,看不懂了,不要懷疑本身的智商,回過頭來多看幾遍,我就是這樣作的,一遍有一遍的感覺,等你那天看源碼不禁的驚歎一聲,這個代碼寫得太好了,恭喜你,你已經離優秀不遠了。最後,看源碼,能培養咱們的編程思惟,固然這個層次有點高了,須要時間積累,畢竟我離這個境界也有點遠。html

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

1.繼承關係

繼承三個接口的說明:git

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接口。api

那麼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. 爲何是不可變的

一、什麼是不可變?

從 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參考文檔

追本溯源,方能闊步前行

參考資料:

參考博客:

http://www.javashuo.com/article/p-plmeeqyq-bo.html

http://www.javashuo.com/article/p-tzkctyuv-bn.html

參考書籍: java官方文檔 《深刻理解JVM虛擬機》

相關文章
相關標籤/搜索