String原始碼分析

wustor發表於2019-03-04

概述

在分析String的原始碼之前,打算先介紹一點關於JVM的記憶體分佈,這樣有助於我們更好地去理解String的設計:

JVM記憶體模型
JVM記憶體模型

Method Area:方法區,當虛擬機器裝載一個class檔案時,它會從這個class檔案包含的二進位制資料中解析型別資訊,然後把這些型別資訊(包括類資訊、常量、靜態變數等)放到方法區中,該記憶體區域被所有執行緒共享,本地方法區存在一塊特殊的記憶體區域,叫常量池(Constant Pool)。
Heap:堆是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,Java中的。
Stack:棧,又叫堆疊或者虛擬機器棧。JVM為每個新建立的執行緒都分配一個棧。也就是說,對於一個Java程式來說,它的執行就是通過對棧的操作來完成的。棧以幀為單位儲存執行緒的狀態。JVM對棧只進行兩種操作:以幀為單位的壓棧和出棧操作。我們知道,某個執行緒正在執行的方法稱為此執行緒的當前方法。
Program Count Register:程式計數器,又叫程式暫存器。JVM支援多個執行緒同時執行,當每一個新執行緒被建立時,它都將得到它自己的PC暫存器(程式計數器)。如果執行緒正在執行的是一個Java方法(非native),那麼PC暫存器的值將總是指向下一條將被執行的指令,如果方法是 native的,程式計數器暫存器的值不會被定義。 JVM的程式計數器暫存器的寬度足夠保證可以持有一個返回地址或者native的指標。
Native Stack:本地方法棧,儲存本地方方法的呼叫狀態。

常量池(constant pool)指的是在編譯期被確定,並被儲存在已編譯的.class檔案中的一些資料。它包括了關於類、方法、介面等中的常量,也包括字串常量。Java把記憶體分為堆記憶體跟棧記憶體,前者主要用來存放物件,後者用於存放基本型別變數以及物件的引用。

正文

繼承關係

先看一下文件中的註釋。

  • 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:
  • String 字串是常量,其值在例項建立後就不能被修改,但字串緩衝區支援可變的字串,因為緩衝區裡面的不可變字串物件們可以被共。
String繼承體系
String繼承體系

通過註釋跟繼承關係,我們知道String被final修飾,而且一旦建立就不能更改,並且實現了CharSequence,Comparable以及Serializable介面。

final:

  • 修飾類:當用final修飾一個類時,表明這個類不能被繼承。也就是說,String類是不能被繼承的,
  • 修飾方法:把方法鎖定,以防任何繼承類修改它的含義。
  • 修飾變數:修飾基本資料型別變數,則其數值一旦在初始化之後便不能更改;如果是引用型別的變數,則在對其初始化之後便不能再讓其指向另一個物件。

String類通過final修飾,不可被繼承,同時String底層的字元陣列也是被final修飾的,char屬於基本資料型別,一旦被賦值之後也是不能被修改的,所以String是不可變的。

CharSequence

CharSequence翻譯過來就是字串,String我們平常也是叫作字串,但是前者是一個介面,下面看一下介面裡面的方法:

    int length();
    char charAt(int index);
    CharSequence subSequence(int start, int end);
    public String toString();
    }複製程式碼

方法很少,並沒有看到我們常見的String的方法,這個類應該只是一個通用的介面,那麼翻一翻它的實現類

CharSequence實現類
CharSequence實現類

CharSequence的實現類裡面出現了我們很常見的StringBuilder跟StringBuffer,先放一放,一會兒再去研究他們倆。

成員變數

private final char value[];//final字元陣列,一旦賦值,不可更改
private int hash;  //快取String的 hash Code,預設值為 0
private static final ObjectStreamField[] serialPersistentFields =new ObjectStreamField[0];//儲存物件的序列化資訊複製程式碼

構造方法

空引數初始化

 public String(){
  this.value = "".value;
}
//將陣列的值初始化為空串,此時在棧記憶體中建立了一個引用,在堆記憶體中建立了一個物件
//示例程式碼
String str = new String()
str = "hello";複製程式碼
  • 1.先建立了一個空的String物件
  • 2.接著又在常量池中建立了一個”hello”,並賦值給第二個String
  • 3.將第二個String的引用傳遞給第一個String

這種方式實際上建立了兩個物件

String初始化

public String(String original){
  this.value = original.value;
  this.hash = original.hash;
}
//程式碼示例
String str=new String("hello")複製程式碼

建立了一個物件

字元陣列初始化

public String(char value[]){
//將傳過來的char拷貝至value陣列裡面
    this.value = Arrays.copyOf(value, value.length);
}複製程式碼

位元組陣列初始化

不指定編碼

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

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{
  //預設使用 ISO-8859-1 編碼格式進行編碼操作
    return decode("ISO-8859-1", ba, off, len);  } catch(UnsupportedEncodingException x){
    //異常捕獲}複製程式碼

指定編碼

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[] 陣列,而又保證不出現亂碼,那就要指定其解碼方式

通過”SB”構造

···
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;
    }複製程式碼
  • 1.先判斷兩個物件的地址是否相等
    1. 再判斷是否是String型別
  • 3.如果都是String型別,就先比較長度是否相等,然後在比較值

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;
    }複製程式碼
  • 1.如果String的length==0或者hash值為0,則直接返回0
  • 2.上述條件不滿足,則通過演算法s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]計算hash值
    我們知道,hash值很多時候用來判斷兩個物件的值是否相等,所以需要儘可能保證唯一性,前面在分析HashMap原理的時候曾經提到過,衝突越少查詢的效率也就越高。

intern方法

 public native String intern();複製程式碼
  • Returns a canonical representation for the string object. A pool of strings, initially empty, is maintained privately by the class . When the intern method is invoked, if the pool already contains a string equal to this object as determined by the method, then the string from the pool is returned. Otherwise, this object is added to the pool and a reference to this object is returned. It follows that for any two strings { s} and { t}, { s.intern() == t.intern()} is { true}if and only if {s.equals(t)} is { true}.
  • 返回一個當前String的一個固定表示形式。String的常量池,初始化為空,被當前類維護。當此方法被呼叫的時候,如果常量池中包含有跟當前String值相等的常量,這個常量就會被返回。否則,當前string的值就會被加入常量池,然後返回當前String的引用。如果兩個String的intern()呼叫==時返回true,那麼equals方法也是true.

翻譯完了,其實就是一句話,如果常量池中有當前String的值,就返回這個值,如果沒有就加進去,返回這個值的引用,看起來很厲害的樣子。

String對“+”的過載

我們知道,”+”跟”+=”是Java中僅有的兩個過載操作符,除此之外,Java不支援其它的任何過載操作符,下面通過反編譯來看一下Java是如何進行過載的:

public static void main(String[] args) {
     String str1="wustor";
     String str2= str1+ "Android";
}複製程式碼

反編譯Main.java,執行命令 javap -c Main,輸出結果

反編譯Main檔案
反編譯Main檔案

可能看不懂所有的程式碼,但是我們看到了StringBuilder,然後看到了wustor跟Android,以及呼叫了StringBuilder的append方法。既然編譯器已經在底層為我們進行優化,那麼為什麼還要提倡我們有StringBuilder呢?
我們仔細觀察一下上面的第三行程式碼,new 了一個StringBuilder物件,如果有是在一個迴圈裡面,我們使用”+”號進行過載的話就會建立多個StringBuilder的物件,而且,即時編譯器都幫我們優化了,但是編譯器事先是不知道我們StringBuilder的長度的,並不能事先分配好緩衝區,也會加大記憶體的開銷,而且使用過載的時候根據java的記憶體分配也會建立多個物件,那麼為什麼要使用StringBuilder呢,我們稍後會分析。

switch

String的Switch原理
String的Switch原理
  • 1.首先呼叫String的HashCode方法,拿到相應的Code
  • 2.通過這個code然後給每個case唯一的標識
  • 3.通過標識來執行相應的操作

我覺得挺好奇,所以接著檢視一下如果是char型別的看看switch是怎麼轉換的

    public static void main(String[] args) {
        char ch = `a`;
        switch (ch) {
            case `a`:
                System.out.println("hello");
                break;
            case `b`:
                System.out.println("world");
                break;
            default:
                break;
        }
    }複製程式碼
Char的Switch語句
Char的Switch語句

基本上跟String差不多,就不多解釋了,由此可以看出,Java對String的Switch支援實際上也還是對int型別的支援。

StringBuilder

由於String物件是不可變的,所以在過載的時候會建立多個物件,而StringBuilder物件是可變的,可以直接使用append方法來進行拼接,下面看看StringBuilder的拼接。

StringBuilder繼承關係
StringBuilder繼承關係
public final class StringBuilder extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

     // 空的構造方法
    public StringBuilder () {
        super(16);
    }
    //給予一個初始化容量
    public StringBuffer(int capacity) {
        super(capacity);
    }
    //使用String進行建立
    public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }
  @Override
    public StringBuilder append(CharSequence s) {
        super.append(s);
        return this;
    }複製程式碼

我們看到StringBuilder都是在呼叫父類的方法,而且通過繼承關係,我們知道它是AbstractStringBuilder 的子類,那我們就繼續檢視它的父類,AbstractStringBuilder 實現了Appendable跟CharSequence 介面,所以它能夠跟String相互轉換

成員變數

    char[] value;//字元陣列
    int count;//字元數量複製程式碼

構造方法

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

可以看到AbstractStringBuilder只有兩個構造方法,一個為空實現,還有一個為指定字元陣列的容量,如果事先知道String的長度,並且這個長度小於16,那麼就可以節省記憶體空間。他的陣列和String的不一樣,因為成員變數value陣列沒有被final修飾所以可以修改他的引用變數的值,即可以引用到新的陣列物件。所以StringBuilder物件是可變的

append方法

append方法
append方法

通過圖片可以看到,append有很多過載方法,其實原理都差不多,我們拿char舉例子

  @Override
    public AbstractStringBuilder append(char c) {
        ensureCapacityInternal(count + 1);//檢測容量
        value[count++] = c;
        return this;
    }
    //判斷當前位元組陣列的容量是否滿足需求
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
        //目前所需容量超出value陣列的容量,進行擴容
            expandCapacity(minimumCapacity);
    }
    //開始擴容
    void expandCapacity(int minimumCapacity) {
    //將現有容量擴充至value陣列的2倍多2
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
          //如果擴容後的長度比需要的長度還小,則跟需要的長度進行交換
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        //將陣列擴容拷貝
        value = Arrays.copyOf(value, newCapacity);
    }複製程式碼

insert方法

insert方法
insert方法

insert也有很多過載方法,下面同樣以char為例

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

StringBuffer

StringBuilder繼承關係
StringBuilder繼承關係

跟StringBuilder差不多,只不過在所有的方法上面加了一個同步鎖而已,不再贅述。

equals與==

equals方法:由於String重新了Object的equas方法,所以只要兩個String物件的值一樣,那麼就會返回true.
==:這個比較的是記憶體地址,下面通過大量的程式碼示例,來驗證一下剛才分析的原始碼

建立方式 物件個數 引用指向
String a=”wustor” 1 常量池
String b=new String(“wustor”) 1 堆記憶體
String c=new String() 1 堆記憶體
String d=”wust”+”or” 3 常量池
String e=a+b 3 堆記憶體

其他常用方法

valueOf() 轉換為字串
trim() 去掉起始和結尾的空格
substring() 擷取字串
indexOf() 查詢字元或者子串第一次出現的地方
toCharArray()轉換成字元陣列
getBytes()獲取位元組陣列
charAt() 擷取一個字元 
length() 字串的長度
toLowerCase() 轉換為小寫

總結

  • String被final修飾,一旦被建立,無法更改
  • String類的所有方法都沒有改變字串本身的值,都是返回了一個新的物件。
  • 如果你需要一個可修改的字串,應該使用StringBuilder或者 StringBuffer。
  • 如果你只需要建立一個字串,你可以使用雙引號的方式,如果你需要在堆中建立一個新的物件,你可以選擇建構函式的方式。
  • 在使用StringBuilder時儘量指定大小這樣會減少擴容的次數,有助於提升效率。

相關文章