每天都在用String,你真的瞭解嗎?

說故事的五公子發表於2020-08-18

1.String概述

java.lang.String 類代表字串。Java程式中所有的字串文字(例如"abc")都可以被看作是實現此類的例項

String 中包括用於檢查各個字串的方法,比如用於比較字串,搜尋字串,提取子字串以及建立具有翻譯為大寫或小寫的所有字元的字串的副本。

2.String原始碼分析

2.1.String成員變數

// String的屬性值,String的內容本質上是使用不可變的char型別的陣列來儲存的。
private final char value[];

/*String型別的hash值,hash是String例項化物件的hashcode的一個快取值,這是因為String物件經常被用來進行比較,如果每次比較都重新計算hashcode值的話,是比較麻煩的,儲存一個快取值能夠進行優化 */
private int hash; // Default to 0

//serialVersionUID為序列化ID
private static final long serialVersionUID = -6849794470754667710L;

//serialPersistentFields屬性用於指定哪些欄位需要被預設序列化
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

serialPersistentFields具體用法為:

private static final ObjectStreamField[] serialPersistentFields = {
    new ObjectStreamField("name", String.class),
    new ObjectStreamField("age", Integer.Type)
}

transient用於指定哪些欄位不會被預設序列化,兩者同時使用時,transient會被忽略。

在 Java 9 及之後,String 類的實現改用 byte 陣列儲存字串,同時使用 coder來標識使用了哪種字符集編碼。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
}

2.2.String構造方法

1、空參構造

/**
* final宣告的 value陣列不能修改它的引用,所以在建構函式中一定要初始化value屬性
*/
public String() {
	this.value = "".value;
}

2、用一個String來構造

// 初始化一個新建立的 String 物件,使其表示一個與引數相同的字元序列;換句話說,新建立的字串是該引數字串的副本。 
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

3、使用char陣列構造

// 分配一個新的 String,使其表示字元陣列引數中當前包含的字元序列。
public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}

// 分配一個新的 String,它包含取自字元陣列引數一個子陣列的字元。 
public String(char value[], int offset, int count) 

4、使用int陣列構造

// 分配一個新的 String,它包含 Unicode 程式碼點陣列引數一個子陣列的字元。 
public String(int[] codePoints, int offset, int count) 

5、使用byte陣列構造

// 通過使用平臺的預設字符集解碼指定的 byte 陣列,構造一個新的 String。
public String(byte bytes[]) 
    
// 通過使用平臺的預設字符集解碼指定的 byte 陣列,構造一個新的 String。 
public String(byte[] bytes) 

// 通過使用指定的 charset 解碼指定的 byte 陣列,構造一個新的 String。  
public String(byte[] bytes, Charset charset) 

// 通過使用平臺的預設字符集解碼指定的 byte 子陣列,構造一個新的 String。 
public String(byte[] bytes, int offset, int length) 

// 通過使用指定的 charset 解碼指定的 byte 子陣列,構造一個新的 String。
public String(byte[] bytes, int offset, int length, Charset charset) 
           
// 通過使用指定的字符集解碼指定的 byte 子陣列,構造一個新的 String。 
public String(byte[] bytes, int offset, int length, String charsetName) 
         
//通過使用指定的 charset 解碼指定的 byte 陣列,構造一個新的 String。 
public String(byte[] bytes, String charsetName) 
          

6、使用StringBuffer或者StringBuilder構造

//分配一個新的字串,它包含字串緩衝區引數中當前包含的字元序列。 
public String(StringBuffer buffer) 
          
    
// 分配一個新的字串,它包含字串生成器引數中當前包含的字元序列。
public String(StringBuilder builder) 

3.字串常量池

作為最基礎的引用資料型別,Java 設計者為 String 提供了字串常量池以提高其效能,那麼字串常量池的具體原理是什麼?

3.1常量池的實現思想

  • 字串的分配,和其他的物件分配一樣,耗費高昂的時間與空間代價,作為最基礎的資料型別,大量頻繁的建立字串,極大程度地影響程式的效能
  • JVM為了提高效能和減少記憶體開銷,在例項化字串常量的時候進行了一些優化
    • 為字串開闢一個字串常量池,類似於快取區
    • 建立字串常量時,首先檢視字串常量池是否存在該字串
    • 存在該字串,返回引用例項,不存在,例項化該字串並放入池中
  • 實現的基礎
    • 實現該優化的基礎是因為字串是不可變的,可以不用擔心資料衝突進行共享
    • 執行時例項建立的全域性字串常量池中有一個表,總是為池中每個唯一的字串物件維護一個引用,這就意味著它們一直引用著字串常量池中的物件,所以,在常量池中的這些字串不會被垃圾收集器回收

3.2常量池的記憶體位置

    • 儲存的是物件,每個物件都包含一個與之對應的class
    • JVM只有一個堆區(heap)被所有執行緒共享,堆中不存放基本型別和物件引用,只存放物件本身
    • 物件的由垃圾回收器負責回收,因此大小和生命週期不需要確定
    • 每個執行緒包含一個棧區,棧中只儲存基礎資料型別的物件和自定義物件的引用(不是物件)
    • 每個棧中的資料(原始型別和物件引用)都是私有的
    • 棧分為3個部分:基本型別變數區、執行環境上下文、操作指令區(存放操作指令)
    • 資料大小和生命週期是可以確定的,當沒有引用指向資料時,這個資料就會自動消失
  • 方法區
    • 靜態區,跟堆一樣,被所有的執行緒共享
    • 方法區中包含的都是在整個程式中永遠唯一的元素,如class,static變數

字串常量池則存在於方法區

3.3案例分析

String str1 = "abc";
String str2 = "abc";
String str3 = "abc";
String str4 = new String("abc");
String str5 = new String("abc");
String str6 = new String("abc");

image-20200818054152095

變數str1到str6的記憶體分佈如圖所示;str1 = "abc"會先去常量池中看有沒有abc,如果有則引用這個字串,沒有則建立一個;str2和str3都是直接引用常量池中的abc;

String str4 = new String("abc") 這段程式碼會做兩步操作,第一步在常量池中查詢是否有"abc"物件,有則返回對應的引用例項,沒有則建立對應的例項物件;在堆中new一個String("abc")物件,將物件地址賦值給Str4,建立一個引用。

4.String記憶體分析

我們先來看一段程式碼

public class TestString {
    public static void main(String[] args) {
        String str1 = "wugongzi";
        String str2 = new String("wugongzi");
        String str3 = str2; //引用傳遞,str3直接指向st2的堆記憶體地址
        String str4 = "wugongzi";
        /**
         *  ==:
         * 基本資料型別:比較的是基本資料型別的值是否相同
         * 引用資料型別:比較的是引用資料型別的地址值是否相同
         * 所以在這裡的話:String類物件==比較,比較的是地址,而不是內容
         */
         System.out.println(str1==str2);//false
         System.out.println(str1==str3);//false
         System.out.println(str3==str2);//true
         System.out.println(str1==str4);//true
    }
}

下面我們來分析一下這段程式碼的記憶體分佈

image-20200818054357

第一步:String str1 = "wugongzi" ,首先會去常量池中看有沒有wugongzi,發現沒有,則在常量池中建立了一個wugongzi,然後將wugongzi的記憶體地址賦值給str1;

第二步:String str2 = new String("wugongzi"),這段程式碼因為new了一個String物件,它首先常量池中查詢是否有wugongzi,發現已經有了,則返回對應的引用例項;然後再去堆中new一個String("wugongzi")物件,將物件地址賦值給Str2,建立一個引用。

第三步:String str3 = str2,// 引用傳遞,str3直接指向st2的堆記憶體地址;

第四步:String str4 = "wugongzi",同第一步

5.String常用方法

5.1.equals方法

這裡重寫了Object中的equals方法,用來判斷兩個物件實際意義上是否相等,也就是值是否相等

public boolean equals(Object anObject) {
    //如果引用的是同一個物件,則返回真
    if (this == anObject) {
        return true;
    }
    //如果不是String型別的資料,返回假
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = value.length;
        //如果char陣列長度不相等,返回假
        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;
}

5.2.compareTo方法

用於比較兩個字串的大小,如果兩個字串長度相等則返回0,如果長度不相等,則返回當前字串的長度減去被比較的字串的長度。

public int compareTo(String anotherString) {
    //自身物件字串長度len1
    int len1 = value.length;
    //被比較物件字串長度len2
    int len2 = anotherString.value.length;
    //取兩個字串長度的最小值lim
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;
 
    int k = 0;
    //從value的第一個字元開始到最小長度lim處為止,如果字元不相等,返回自身(物件不相等處字元-被比較物件不相等字元)
    while (k < lim) {
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) {
            return c1 - c2;
        }
        k++;
    }
    //如果前面都相等,則返回(自身長度-被比較物件長度)
    return len1 - len2;
}

5.3.hashCode方法

這裡重寫了hashCode方法,採用多項式進行計算,可以通過不同的字串得到相同的hash,所以兩個String物件的hashCode相同,並不代表兩個String是相同的。

演算法:假設n = 3

i=0 -> h = 31 * 0 + val[0]

i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]

i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]

​ h = 3131310 + 3131val[0] + 31val[1] + val[2]

​ h = 31^(n-1)val[0] + 31^(n-2)val[1] + val[2]

public int hashCode() {
    int h = hash;
    //如果hash沒有被計算過,並且字串不為空,則進行hashCode計算
    if (h == 0 && value.length > 0) {
        char val[] = value;
 
        //計算過程
        //s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        //hash賦值
        hash = h;
    }
    return h;
}

5.4.startWith方法

startsWith和endWith方法也是比較常用的方法,常用來判斷字串以特定的字元開始或結尾。

public boolean startsWith(String prefix, int toffset) {
    char ta[] = value;
    int to = toffset;
    char pa[] = prefix.value;
    int po = 0;
    int pc = prefix.value.length;
    // Note: toffset might be near -1>>>1.
    //如果起始地址小於0或者(起始地址+所比較物件長度)大於自身物件長度,返回假
    if ((toffset < 0) || (toffset > value.length - pc)) {
        return false;
    }
    //從所比較物件的末尾開始比較
    while (--pc >= 0) {
        if (ta[to++] != pa[po++]) {
            return false;
        }
    }
    return true;
}
 
public boolean startsWith(String prefix) {
    return startsWith(prefix, 0);
}
 
public boolean endsWith(String suffix) {
    return startsWith(suffix, value.length - suffix.value.length);
}

5.5.concat方法

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

5.6.replace方法

replace的引數是char和charSequence,即可以支援字元的替換,也支援字串的替換(charSequence即字串序列的意思)

replaceAll的引數是regex,即基於規則表示式的替換,比如可以通過replaceAll("\d","*")把一個字串所有的數字字元都替換成星號;

相同點:都是全部替換,即把源字串中的某一字元或者字串全部替換成指定的字元或者字串。

不同點:replaceAll支援正規表示式,因此會對引數進行解析(兩個引數均是),如replaceAll("\d",""),而replace則不會,replace("\d","")就是替換"\d"的字串,而不會解析為正則。

public String replace(char oldChar, char newChar) {
    //新舊值先對比
    if (oldChar != newChar) {
        int len = value.length;
        int i = -1;
        char[] val = value; 
 
        //找到舊值最開始出現的位置
        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        //從那個位置開始,直到末尾,用新值代替出現的舊值
        if (i < len) {
            char buf[] = new char[len];
            for (int j = 0; j < i; j++) {
                buf[j] = val[j];
            }
            while (i < len) {
                char c = val[i];
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            }
            return new String(buf, true);
        }
    }
    return this;
}

5.7.trim方法

trim用於刪除字串的頭尾的空格。

public String trim() {
    int len = value.length;
    int st = 0;
    char[] val = value;    /* avoid getfield opcode */
 
    //找到字串前段沒有空格的位置
    while ((st < len) && (val[st] <= ' ')) {
        st++;
    }
    //找到字串末尾沒有空格的位置
    while ((st < len) && (val[len - 1] <= ' ')) {
        len--;
    }
    //如果前後都沒有出現空格,返回字串本身
    return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

5.8.其他方法

//字串是否包含另一個字串
public boolean contains(CharSequence s)

//返回字串長度
public int length()

//返回在指定index位置的字元,index從0開始
public char charAt(int index)

//返回str字串在當前字串首次出現的位置,若沒有返回-1
public int indexOf(String str)

//返回str字串最後一次在當前字串中出現的位置,若無返回-1
public int lastIndexOf(String str)

//返回s字串從當前字串startpoint位置開始的,首次出現的位置
public int indexOf(String s ,int startpoint)

//返回s字串從當前字串startpoint位置開始的,最後一次出現的位置
public int lastIndexOf(String s ,int startpoint)

//返回從start開始的子串
public String substring(int startpoint)

//返回從start開始到end結束的一個左閉右開的子串。start可以從0開始的
public String substring(int start,int end)

//按照regex將當前字串拆分,拆分為多個字串,整體返回值為String[]
public String[] split(String regex)

6.String常用轉化

6.1字串 --->基本資料型別、包裝類

呼叫相應的包裝類的parseXxx(String str);

String str1 = "wugongzi";
int i = Integer.parseInt(str1);
System.out.println(i);

6.2字串---->位元組陣列

呼叫字串的getBytes()

String str = "wugongzi520";
byte[] b = str.getBytes();
for(int j = 0;j < b.length;j++){
    System.out.println((char)b[j]);
}

6.3位元組陣列---->字串

呼叫字串的構造器

String str = "wugongzi520";
byte[] b = str.getBytes();
String str3 = new String(b);
System.out.println(str3);

6.4字串---->字元陣列

呼叫字串的toCharArray();

String str4 = "abc123";
char[] c = str4.toCharArray();
for(int j = 0;j < c.length;j++){
    System.out.println(c[j]);
}

6.5字元陣列---->字串

呼叫字串的構造器

參考:

https://segmentfault.com/a/1190000009888357

https://www.cnblogs.com/liudblog/p/11196293.html

相關文章