JDK1.8源碼(三)——java.lang.String 類

  String 類也是java.lang 包下的一個類,算是平常編碼中最經常使用的一個類了,那麼本篇博客就來詳細的介紹 String 類。html

一、String 類的定義

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {}

  和上一篇博客所講的 Integer 類同樣,這也是一個用 final 聲明的常量類,不能被任何類所繼承,並且一旦一個String對象被建立, 包含在這個對象中的字符序列是不可改變的, 包括該類後續的全部方法都是不能修改該對象的,直至該對象被銷燬,這是咱們須要特別注意的(該類的一些方法看似改變了字符串,其實內部都是建立一個新的字符串,下面講解方法時會介紹)。接着實現了 Serializable接口,這是一個序列化標誌接口,還實現了 Comparable 接口,用於比較兩個字符串的大小(按順序比較單個字符的ASCII碼),後面會有具體方法實現;最後實現了 CharSequence 接口,表示是一個有序字符的集合,相應的方法後面也會介紹。java

二、字段屬性

/**用來存儲字符串  */
private final char value[];

/** 緩存字符串的哈希碼 */
private int hash; // Default to 0

/** 實現序列化的標識 */
private static final long serialVersionUID = -6849794470754667710L;

  一個 String 字符串其實是一個 char 數組。正則表達式

三、構造方法

  String 類的構造方法不少。能夠經過初始化一個字符串,或者字符數組,或者字節數組等等來建立一個 String 對象。
  
算法

String str1 = "abc";//注意這種字面量聲明的區別,文末會詳細介紹
String str2 = new String("abc");
String str3 = new String(new char[]{'a','b','c'});

四、equals(Object anObject) 方法

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 類重寫了 equals 方法,比較的是組成字符串的每個字符是否相同,若是都相同則返回true,不然返回false。數據庫

五、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 類的 hashCode 算法很簡單,主要就是中間的 for 循環,計算公式以下:編程

s[0] 31^(n-1) + s[1]31^(n-2) + ... + s[n-1]

  s 數組即源碼中的 val 數組,也就是構成字符串的字符數組。這裏有個數字 31 ,爲何選擇31做爲乘積因子,並且沒有用一個常量來聲明?主要緣由有兩個:segmentfault

  ①、31是一個不大不小的質數,是做爲 hashCode 乘子的優選質數之一。api

  ②、31能夠被 JVM 優化,31 * i = (i << 5) - i。由於移位運算比乘法運行更快更省性能。數組

六、charAt(int index) 方法

public char charAt(int index) {
        //若是傳入的索引大於字符串的長度或者小於0,直接拋出索引越界異常
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];//返回指定索引的單個字符
    }

  咱們知道一個字符串是由一個字符數組組成,這個方法是經過傳入的索引(數組下標),返回指定索引的單個字符。緩存

七、compareTo(String anotherString) 和 compareToIgnoreCase(String str) 方法

  咱們先看看 compareTo 方法:

public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }

  源碼也很好理解,該方法是按字母順序比較兩個字符串,是基於字符串中每一個字符的 Unicode 值。當兩個字符串某個位置的字符不一樣時,返回的是這一位置的字符 Unicode 值之差,當兩個字符串都相同時,返回兩個字符串長度之差。

  compareToIgnoreCase() 方法在 compareTo 方法的基礎上忽略大小寫,咱們知道大寫字母是比小寫字母的Unicode值小32的,底層實現是先都轉換成大寫比較,而後都轉換成小寫進行比較。
### 八、concat(String str) 方法
  該方法是將指定的字符串鏈接到此字符串的末尾。

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

  首先判斷要拼接的字符串長度是否爲0,若是爲0,則直接返回原字符串。若是不爲0,則經過 Arrays 工具類(後面會詳細介紹這個工具類)的copyOf方法建立一個新的字符數組,長度爲原字符串和要拼接的字符串之和,前面填充原字符串,後面爲空。接着在經過 getChars 方法將要拼接的字符串放入新字符串後面爲空的位置。

  注意:返回值是 new String(buf, true),也就是從新經過 new 關鍵字建立了一個新的字符串,原字符串是不變的。這也是前面咱們說的一旦一個String對象被建立, 包含在這個對象中的字符序列是不可改變的。

九、indexOf(int ch) 和 indexOf(int ch, int fromIndex) 方法

  indexOf(int ch),參數 ch 實際上是字符的 Unicode 值,這裏也能夠放單個字符(默認轉成int),做用是返回指定字符第一次出現的此字符串中的索引。其內部是調用 indexOf(int ch, int fromIndex),只不過這裏的 fromIndex =0 ,由於是從 0 開始搜索;而 indexOf(int ch, int fromIndex) 做用也是返回首次出現的此字符串內的索引,可是從指定索引處開始搜索。  

public int indexOf(int ch) {
        return indexOf(ch, 0);//從第一個字符開始搜索
    }
public int indexOf(int ch, int fromIndex) {
    final int max = value.length;//max等於字符的長度
    if (fromIndex < 0) {//指定索引的位置若是小於0,默認從 0 開始搜索
        fromIndex = 0;
    } else if (fromIndex >= max) {
        //若是指定索引值大於等於字符的長度(由於是數組,下標最多隻能是max-1),直接返回-1
        return -1;
    }

    if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {//一個char佔用兩個字節,若是ch小於2的16次方(65536),絕大多數字符都在此範圍內
        final char[] value = this.value;
        for (int i = fromIndex; i < max; i++) {//for循環依次判斷字符串每一個字符是否和指定字符相等
            if (value[i] == ch) {
                return i;//存在相等的字符,返回第一次出現該字符的索引位置,並終止循環
            }
        }
        return -1;//不存在相等的字符,則返回 -1
    } else {//當字符大於 65536時,處理的少數狀況,該方法會首先判斷是不是有效字符,而後依次進行比較
        return indexOfSupplementary(ch, fromIndex);
    }
}

十、split(String regex) 和 split(String regex, int limit) 方法

  split(String regex) 將該字符串拆分爲給定正則表達式的匹配。split(String regex , int limit) 也是同樣,不過對於 limit 的取值有三種狀況:

  ①、limit > 0 ,則pattern(模式)應用n - 1 次

String str = "a,b,c";
 String[] c1 = str.split(",", 2);
 System.out.println(c1.length);//2
 System.out.println(Arrays.toString(c1));//{"a","b,c"}

  ②、limit = 0 ,則pattern(模式)應用無限次而且省略末尾的空字串

String str2 = "a,b,c,,";
String[] c2 = str2.split(",", 0);
System.out.println(c2.length);//3
System.out.println(Arrays.toString(c2));//{"a","b","c"}

  ③、limit < 0 ,則pattern(模式)應用無限次

String str2 = "a,b,c,,";
 String[] c2 = str2.split(",", -1);
 System.out.println(c2.length);//5
 System.out.println(Arrays.toString(c2));//{"a","b","c","",""}

  下面咱們看看底層的源碼實現。對於 split(String regex) 沒什麼好說的,內部調用 split(regex, 0) 方法:

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

  重點看 split(String regex, int limit) 的方法實現:

public String[] split(String regex, int limit) {
    /* 一、單個字符,且不是".$|()[{^?*+\\"其中一個
     * 二、兩個字符,第一個是"\",第二個大小寫字母或者數字
     */
    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;//大於0,limited==true,反之limited==false
        ArrayList<String> list = new ArrayList<>();
        while ((next = indexOf(ch, off)) != -1) {
            //當參數limit<=0 或者 集合list的長度小於 limit-1
            if (!limited || list.size() < limit - 1) {
                list.add(substring(off, next));
                off = next + 1;
            } else {//判斷最後一個list.size() == limit - 1
                list.add(substring(off, value.length));
                off = value.length;
                break;
            }
        }
        //若是沒有一個能匹配的,返回一個新的字符串,內容和原來的同樣
        if (off == 0)
            return new String[]{this};

        // 當 limit<=0 時,limited==false,或者集合的長度 小於 limit是,截取添加剩下的字符串
        if (!limited || list.size() < limit)
            list.add(substring(off, value.length));

        // 當 limit == 0 時,若是末尾添加的元素爲空(長度爲0),則集合長度不斷減1,直到末尾不爲空
        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);
}

十一、replace(char oldChar, char newChar) 和 String replaceAll(String regex, String replacement) 方法

  ①、replace(char oldChar, char newChar) :將原字符串中全部的oldChar字符都替換成newChar字符,返回一個新的字符串。

  ②、String replaceAll(String regex, String replacement):將匹配正則表達式regex的匹配項都替換成replacement字符串,返回一個新的字符串。

十二、substring(int beginIndex) 和 substring(int beginIndex, int endIndex) 方法

  ①、substring(int beginIndex):返回一個從索引 beginIndex 開始一直到結尾的子字符串。

public String substring(int beginIndex) {
    if (beginIndex < 0) {//若是索引小於0,直接拋出異常
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;//subLen等於字符串長度減去索引
    if (subLen < 0) {//若是subLen小於0,也是直接拋出異常
        throw new StringIndexOutOfBoundsException(subLen);
    }
    //一、若是索引值beginIdex == 0,直接返回原字符串
    //二、若是不等於0,則返回從beginIndex開始,一直到結尾
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

  ②、 substring(int beginIndex, int endIndex):返回一個從索引 beginIndex 開始,到 endIndex 結尾的子字符串。

1三、常量池

  在前面講解構造函數的時候,咱們知道最多見的兩種聲明一個字符串對象的形式有兩種:

  ①、經過「字面量」的形式直接賦值

String str = "hello";

  ②、經過 new 關鍵字調用構造函數建立對象

String str = new String("hello");

  那麼這兩種聲明方式有什麼區別呢?在講解以前,咱們先介紹 JDK1.7(不包括1.7)之前的 JVM 的內存分佈:
  

  ①、程序計數器:也稱爲 PC 寄存器,保存的是程序當前執行的指令的地址(也能夠說保存下一條指令的所在存儲單元的地址),當CPU須要執行指令時,須要從程序計數器中獲得當前須要執行的指令所在存儲單元的地址,而後根據獲得的地址獲取到指令,在獲得指令以後,程序計數器便自動加1或者根據轉移指針獲得下一條指令的地址,如此循環,直至執行完全部的指令。線程私有。

  ②、虛擬機棧:基本數據類型、對象的引用都存放在這。線程私有。

  ③、本地方法棧:虛擬機棧是爲執行Java方法服務的,而本地方法棧則是爲執行本地方法(Native Method)服務的。在JVM規範中,並無對本地方法棧的具體實現方法以及數據結構做強制規定,虛擬機能夠自由實現它。在HotSopt虛擬機中直接就把本地方法棧和虛擬機棧合二爲一。

  ④、方法區:存儲了每一個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯後的代碼等。注意:在Class文件中除了類的字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。

  ⑤、堆:用來存儲對象自己的以及數組(固然,數組引用是存放在Java棧中的)。

  在 JDK1.7 之後,方法區的常量池被移除放到堆中了,以下:
  

  常量池:Java運行時會維護一個String Pool(String池), 也叫「字符串緩衝區」。String池用來存放運行時中產生的各類字符串,而且池中的字符串的內容不重複。

  ①、字面量建立字符串或者純字符串(常量)拼接字符串會先在字符串池中找,看是否有相等的對象,沒有的話就在字符串池建立該對象;有的話則直接用池中的引用,避免重複建立對象。

  ②、new關鍵字建立時,直接在堆中建立一個新對象,變量所引用的都是這個新對象的地址,可是若是經過new關鍵字建立的字符串內容在常量池中存在了,那麼會由堆在指向常量池的對應字符;可是反過來,若是經過new關鍵字建立的字符串對象在常量池中沒有,那麼經過new關鍵詞建立的字符串對象是不會額外在常量池中維護的。

  ③、使用包含變量表達式來建立String對象,則不只會檢查維護字符串池,還會在堆區建立這個對象,最後是指向堆內存的對象。

String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");
System.out.println(str1==str2);//true
System.out.println(str1==str3);//fasle
System.out.println(str2==str3);//fasle
System.out.println(str1.equals(str2));//true
System.out.println(str1.equals(str3));//true
System.out.println(str2.equals(str3));//true

  對於上面的狀況,首先 String str1 = "hello",會先到常量池中檢查是否有「hello」的存在,發現是沒有的,因而在常量池中建立「hello」對象,並將常量池中的引用賦值給str1;第二個字面量 String str2 = "hello",在常量池中檢測到該對象了,直接將引用賦值給str2;第三個是經過new關鍵字建立的對象,常量池中有了該對象了,不用在常量池中建立,而後在堆中建立該對象後,將堆中對象的引用賦值給str3,再將該對象指向常量池。以下圖所示:
  

  注意:看上圖紅色的箭頭,經過 new 關鍵字建立的字符串對象,若是常量池中存在了,會將堆中建立的對象指向常量池的引用。咱們能夠經過文章末尾介紹的intern()方法來驗證。

  使用包含變量表達式建立對象:

String str1 = "hello";
String str2 = "helloworld";
String str3 = str1+"world";//編譯器不能肯定爲常量(會在堆區建立一個String對象)
String str4 = "hello"+"world";//編譯器肯定爲常量,直接到常量池中引用

System.out.println(str2==str3);//fasle
System.out.println(str2==str4);//true
System.out.println(str3==str4);//fasle

  str3 因爲含有變量str1,編譯器不能肯定是常量,會在堆區中建立一個String對象。而str4是兩個常量相加,直接引用常量池中的對象便可。
  

1四、intern() 方法

  這是一個本地方法:

public native String intern();

  當調用intern方法時,若是池中已經包含一個與該String肯定的字符串相同equals(Object)的字符串,則返回該字符串。不然,將此String對象添加到池中,並返回此對象的引用。

  這句話什麼意思呢?就是說調用一個String對象的intern()方法,若是常量池中有該對象了,直接返回該字符串的引用(存在堆中就返回堆中,存在池中就返回池中),若是沒有,則將該對象添加到池中,並返回池中的引用。

String str1 = "hello";//字面量 只會在常量池中建立對象
String str2 = str1.intern();
System.out.println(str1==str2);//true

String str3 = new String("world");//new 關鍵字只會在堆中建立對象
String str4 = str3.intern();
System.out.println(str3 == str4);//false

String str5 = str1 + str2;//變量拼接的字符串,會在常量池中和堆中都建立對象
String str6 = str5.intern();//這裏因爲池中已經有對象了,直接返回的是對象自己,也就是堆中的對象
System.out.println(str5 == str6);//true

String str7 = "hello1" + "world1";//常量拼接的字符串,只會在常量池中建立對象
String str8 = str7.intern();
System.out.println(str7 == str8);//true

1五、String 真的不可變嗎?

  前面咱們介紹了,String 類是用 final 關鍵字修飾的,因此咱們認爲其是不可變對象。可是真的不可變嗎?

  每一個字符串都是由許多單個字符組成的,咱們知道其源碼是由 char[] value 字符數組構成。

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

  value 被 final 修飾,只能保證引用不被改變,可是 value 所指向的堆中的數組,纔是真實的數據,只要可以操做堆中的數組,依舊能改變數據。並且 value 是基本類型構成,那麼必定是可變的,即便被聲明爲 private,咱們也能夠經過反射來改變。

String str = "vae";
//打印原字符串
System.out.println(str);//vae
//獲取String類中的value字段 
Field fieldStr = String.class.getDeclaredField("value");
//由於value是private聲明的,這裏修改其訪問權限
fieldStr.setAccessible(true);
//獲取str對象上的value屬性的值
char[] value = (char[]) fieldStr.get(str);
//將第一個字符修改成 V(小寫改大寫)
value[0] = 'V';
//打印修改以後的字符串
System.out.println(str);//Vae

  經過先後兩次打印的結果,咱們能夠看到 String 被改變了,可是在代碼裏,幾乎不會使用反射的機制去操做 String 字符串,因此,咱們會認爲 String 類型是不可變的。

  那麼,String 類爲何要這樣設計成不可變呢?咱們能夠從性能以及安全方面來考慮:

安全

引起安全問題,譬如,數據庫的用戶名、密碼都是以字符串的形式傳入來得到數據庫的鏈接,或者在socket編程中,主機名和端口都是以字符串的形式傳入。由於字符串是不可變的,因此它的值是不可改變的,不然黑客們能夠鑽到空子,改變字符串指向的對象的值,形成安全漏洞。

保證線程安全,在併發場景下,多個線程同時讀寫資源時,會引競態條件,因爲 String 是不可變的,不會引起線程的問題而保證了線程。

HashCode,當 String 被建立出來的時候,hashcode也會隨之被緩存,hashcode的計算與value有關,若 String 可變,那麼 hashcode 也會隨之變化,針對於 Map、Set 等容器,他們的鍵值須要保證惟一性和一致性,所以,String 的不可變性使其比其餘對象更適合當容器的鍵值。

性能

當字符串是不可變時,字符串常量池纔有意義。字符串常量池的出現,能夠減小建立相同字面量的字符串,讓不一樣的引用指向池中同一個字符串,爲運行時節約不少的堆內存。若字符串可變,字符串常量池失去意義,基於常量池的String.intern()方法也失效,每次建立新的 String 將在堆內開闢出新的空間,佔據更多的內存

參考文檔:

https://docs.oracle.com/javas...

https://segmentfault.com/a/11...

本系列教程持續更新,能夠微信搜索「 IT可樂 」第一時間閱讀。回覆《電子書》有我爲你們特別篩選的書籍資料

img

相關文章
相關標籤/搜索