String源碼分析

概述

在分析String的源碼以前,打算先介紹一點關於JVM的內存分佈,這樣有助於咱們更好地去理解String的設計:
java

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是不可變的。bash

CharSequence

CharSequence翻譯過來就是字符串,String咱們日常也是叫做字符串,可是前者是一個接口,下面看一下接口裏面的方法:網絡

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

方法不多,並無看到咱們常見的String的方法,這個類應該只是一個通用的接口,那麼翻一翻它的實現類
app

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

這種方式實際上建立了兩個對象ide

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時儘可能指定大小這樣會減小擴容的次數,有助於提高效率。
相關文章
相關標籤/搜索