String原始碼分析

wustor發表於2019-03-04

其實String方面的面試題往深了延申的話,還是會延伸到JVM,所以還是希望讀者對JVM有一定的瞭解,這樣更便於理解String的設計。


String原始碼分析

String結構

/*
Strings are constant; their values can not be changed after they are created.
Stringbuffers support mutable strings.Because String objects are immutable they can be shared. Forexample:
*/
public final class String implements java.io.Serializable, 
        Comparable<String>, CharSequence 
複製程式碼
原始碼裡可以看到String被final修飾並繼承了三個介面
原始碼註釋也說到字串是不變的; 它們的值在建立後無法更改.Stringbuffers支援可變字串。
因為String物件是不可變的,所以它們可以共享
複製程式碼

String原始碼分析

final

 修飾類:類不可被繼承,也就是說,String類不可被繼承了
 修飾方法:把方法鎖定,以訪任何繼承類修改它的涵義
 修飾遍歷:初始化後不可更改
複製程式碼

Comparable和Serializable

Serializable介面裡為空
Comparable介面裡只有一個public int compareTo(T o);方法
這兩個介面不做敘述.
複製程式碼

CharSequence

介面中的方法
    length(): int
    charAt(): char
    subSequence(int,int):CharSwquence 
    toString(): String 
    chars(): IntStream
    codePoints(): IntStream
複製程式碼
我們發現這個介面中的方法很少,沒有我們常用的String方法,
那麼它應該是一個通用介面,會有很多實現類,包括StringBuilder和StringBuffer
複製程式碼

String構造方法

空參構造

 public String() {
     this.value = "".value;
 }
複製程式碼

解析

String str=new String("abc");
1.先建立一個空的String物件
2.常量池中建立一個abc,並賦值給第二個String
3.將第二個String的引用傳遞給第一個String
注:如果常量池中有abc,則不用建立,直接把引用傳遞給第一個String
複製程式碼

String型別初始化

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

案例:  String str=new String("str");  
複製程式碼

字元陣列初始化

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

注:將傳過來的char陣列copy到value陣列裡
複製程式碼

位元組陣列初始化

byte型別的方法有8個,兩個過時的
剩下六個又分為指定編碼和不指定編碼

不指定編碼

public String(byte bytes[]) {
        this(bytes, 0, bytes.length);
    }

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);
    }
複製程式碼

指定編碼

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是網路傳輸或儲存的序列化形式,
所以在很多傳輸和儲存的過程中需要將byte[]陣列和String進行相互轉化,
byte是位元組,char是字元,位元組流和字元流需要指定編碼,不然可能會亂碼,
bytes位元組流是使用charset進行編碼的,想要將他轉換成unicode的char[]陣列,
而又保證不出現亂碼,那就要指定其解碼方法
複製程式碼

StringBUilder構造

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());
    }

注:很多時候我們不會這麼去構造,因為StringBuilder跟StringBuffer有toString方法
如果不考慮執行緒安全,優先選擇StringBuilder
複製程式碼

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;
    }
複製程式碼
String重寫了父類Object的equals方法
    先判斷地址是否相等(地址相等的情況下,肯定是一個值,直接返回true)
    在判斷是否是String型別,不是則返回false
    如果都是String,先判斷長度,
    再比較值,把值賦給char陣列,遍歷兩個char陣列比較
    
複製程式碼

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;
    }
複製程式碼
如果String的length==0或者hash值為0,則直接返回0
如果上述條件不滿足,則通過演算法計算hash複製程式碼

intern方法

public native String intern();

注:方法註釋會有寫到,意思就是呼叫方法時,
如果常量池有當前String的值,就返回這個值,沒有就加進去,返回這個值的引用
複製程式碼
        String str1="a";
        String str2="b";
        String str3="ab";
        String str4 = str1+str2;
        String str5=new String("ab");

        System.out.println(str5==str3);//堆記憶體比較字串池
        //intern如果常量池有當前String的值,就返回這個值,沒有就加進去,返回這個值的引用
        System.out.println(str5.intern()==str3);//引用的是同一個字串池裡的
        System.out.println(str5.intern()==str4);//變數相加給一個新值,所以str4引用的是個新的
        System.out.println(str4==str3);//變數相加給一個新值,所以str4引用的是個新的
        
        false
        true
        false
        false
複製程式碼

重點: --兩個字串常量或者字面量相加,不會new新的字串,其他相加則是新值,(如 String str5=str1+"b";)

因為在jvm翻譯為二進位制程式碼時,會自動優化,把兩個值後邊的結果先合併,再儲存為一個常量。
複製程式碼

String裡還有很多方法,substring,replace等等,我們就不一一舉例了


StringBuilder

StringBuilder和Stringbuffer這兩個類的方法都很想一樣,因此我們就那StringBuilder的原始碼作分析
等下再去看三者之間的關係和不同
複製程式碼

結構和構造

public final class StringBuilder  extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence{
    
    //空構造,初始大小16
    public StringBuilder() {
        super(16);
    }
    
    //給予一個初始化容量
    public StringBuilder(int capacity) {
        super(capacity);
    }
    
    //使用String進行建立
    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }
    
    //String建立和CharSequence型別建立,額外多分配16個字元的空間,
    //然後呼叫append將引數字元新增進來,(字串緩衝區)
    public StringBuilder(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }
}
複製程式碼

解析

我們可以看到方法內部都是在呼叫父類的方法,
通過繼承關係,我們是知道它的父類是AbstractStringBuilder,
父類裡實現類Appendable跟CharSequence 介面,所以它能夠跟String相互轉換
複製程式碼

父類AbstractStringBuilder

 AbstractStringBuilder() {
    }
    
AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
複製程式碼

解析

父類裡是隻有兩個構造方法,一個為空實現,一個為指定字元陣列的容量,
如果事先知道String的長度小於16,就可以節省記憶體空間,
他的陣列和String的不一樣,因為成員變數value陣列沒有被final修飾,
所以可以修改他的引用變數的值,即可以引用到新的陣列物件,
所以StringBuilder物件是可變的
複製程式碼

append

String原始碼分析

append有很多過載方法,原理都差不多
我們以String舉例

//傳入要追加的字串
public AbstractStringBuilder append(String str) {
        //判斷字串是否為null
        if (str == null)        
            return appendNull();
            
        //不為null,獲得它的長度
        int len = str.length(); 
        
        //呼叫方法,把原先長度+追加字串長度的和傳入方法
        ensureCapacityInternal(count + len);    
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
    
    
    
    //判斷是否滿足擴充套件要求
private void ensureCapacityInternal(int minimumCapacity) {
        // 和-原先字串長度是否>0  肯定是大於0的
        if (minimumCapacity - value.length > 0)
            //呼叫複製空間的方法,和當引數
            expandCapacity(minimumCapacity);
    }
    
    
    //開始擴充
void expandCapacity(int minimumCapacity) {
        //先把原先長度複製2倍多2
        int newCapacity = value.length * 2 + 2;
        
        //判斷newCapacity-和是否<0
        if (newCapacity - minimumCapacity < 0)
            //小於0的情況就是你複製的長度不夠,那就把和的長度給複製的長度
            newCapacity = minimumCapacity;
            
            //正常邏輯怎麼著都走不到這一步,新長度肯定是大於0
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        
        //將陣列擴容拷貝
        value = Arrays.copyOf(value, newCapacity);
    }
    
複製程式碼

insert

String原始碼分析

insert同樣有很多過載方法,下面以char和String為例
insert的ensureCapacityInternal(count + 1);和上面一樣,不做講解了

public AbstractStringBuilder insert(int offset, char c) {
        //檢查是否滿足擴充條件
        ensureCapacityInternal(count + 1);
        //拷貝陣列
        System.arraycopy(value, offset, value, offset + 1, count - offset);
        //進行復制
        value[offset] = c;
        count += 1;
        return this;
    }
    
    
public AbstractStringBuilder insert(int offset, String str) {
        //判斷要插入的座標是否在字串內,不再則報陣列下標越界
        if ((offset < 0) || (offset > length()))
            throw new StringIndexOutOfBoundsException(offset);
        //判斷要插入的是否為null
        if (str == null)
            str = "null";
        //獲得要插入的字串長度    
        int len = str.length();
        //檢查是否滿足擴充條件
        ensureCapacityInternal(count + len);
        //拷貝陣列
        System.arraycopy(value, offset, value, offset + len, count - offset);
        str.getChars(value, offset);
        count += len;
        return this;
    }
複製程式碼

StringBuffer

public final class StringBuffer extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
    
}

跟StringBuilder差不多,只不過在所有的方法上面加了一個同步鎖
複製程式碼

equals與==

equals

String類重寫了父類equals的方法
我們先看下父類的

//直接判斷地址
public boolean equals(Object obj) {
        return (this == obj);
    }
複製程式碼

再看下String類的equals

public boolean equals(Object anObject) {
    //地址相等肯定為true,就不用繼續往下走了
        if (this == anObject) {
            return true;
        }
    //地址不相等的情況下,比較兩個字元的內容是否一樣
    //把字串方法char[]陣列裡,遍歷比較
        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;
    }
複製程式碼

==

==比較的是記憶體地址

基本資料型別比較值
引用資料型別比較地址值
    (物件的引用,在堆空間,String在字串池,newString在堆空間)
複製程式碼

根據下面案例分析一下原始碼

建立方式 物件個數 引用指向
String a="abc"; 1 常量池
String b=new String("abc");; 1 堆記憶體 (abc則是複製的常量池裡的abc)
String c=new String() 1 堆記憶體
String d="a"+"bc"; 3 常量池(a一次,bc一次,和一次,d指向和)
String e=a+b; 3 堆記憶體

重點--兩個字串常量或者字面量相加,不會new新的字串,
變數相加則是會new新的字串,new出來的都在堆


總結

String被final修飾,一旦建立無法更改,每次更改則是在新建立物件
StringBuilder和StringBuffer則是可修改的字串

StringBuilder和StringBuffer的區別
    StringBuffer 被synchronized 修飾,同步,執行緒安全
    StringBuilder並沒有對方法進行加同步鎖,所以是非執行緒安全的。
    如果程式不是多執行緒的,那麼使用StringBuilder效率高於StringBuffer。
複製程式碼

相關文章