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


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 = 31*31*31*0 + 31*31*val[0] + 31*val[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字符數組---->字符串

調用字符串的構造器

7.String長度

面試官:對String了解嗎?

同學:非常熟悉,每天都會用到它。

面試官:String有長度限制嗎?最大能存放多少

同學:這個沒太注意過,我知道int的取值范圍,String也有范圍嗎?

在學習和開發過程中,我們經常會討論 short ,int 和 long 這些基本數據類型的取值范圍,但是對於 String 類型我們好像很少注意它的“取值范圍”。那么對於 String 類型,它到底有沒有長度限制呢?

在日常開發中,大家可能會遇到String超出長度這樣的情況,比如下面這種情況。

String長度

從String的源碼我們可以看出

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

String實際存儲數據的是char數組,數組長度是int類型,而int類型的最大值為2^31 - 1 = 2147483647,所以String最多存儲231-1個字符(注意這里是字符,而不是字節),那既然String可以存儲這么多的字符,為什么還會出現字符串過長呢?我明明沒有放這么多數據啊。

關於這個問題,那就要從兩方面去分析了:

7.1編譯期

通過上面章節的學習,我們知道字符串會存放在字符串常量當中,String長度之所以會受限制,是因為JVM對常量池中的數據長度有限制。常量池中的每一種數據項都有自己的類型。Java中的UTF-8編碼的Unicode字符串在常量池中以CONSTANT_Utf8類型表示。

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

我們可以看到length的類型是u2,u2是無符號的16位整數,最大值為2^16-1=65535,所以String在字符串常量池里的限制為65535個字節(注意這里是字節,而不是字符)

7.2運行期

String 運行時的限制主要體現在 String 的構造函數上。下面是 String 的一個構造函數:

public String(char value[], int offset, int count) {
    ...
}

上面的count值就是字符串的最大長度。在Java中,int的最大長度是2^31-1。所以在運行時,String 的最大長度是2^31-1。

但是這個也是理論上的長度,實際的長度還要看你JVM的內存。我們來看下,最大的字符串會占用多大的內存。

(2^31-1)*2*16/8/1024/1024/1024 = 4GB

所以在最壞的情況下,一個最大的字符串要占用4GB的內存。如果你的虛擬機不能分配這么多內存的話,會直接報錯的。

JDK9以后對String的存儲進行了優化。底層不再使用char數組存儲字符串,而是使用byte數組。對於LATIN1字符的字符串可以節省一倍的內存空間。

7.3總結

String 的長度是有限制的。

  • 編譯期的限制:字符串的UTF8編碼值的字節數不能超過65535,字符串的長度不能超過65534;
  • 運行時限制:字符串的長度不能超過2^31-1,占用的內存數不能超過虛擬機能夠提供的最大值。

參考:

https://segmentfault.com/a/1190000009888357

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM