Java常用API(六)

9.6.3 字符串常量池

1、字符串常量對象可以共享的原因和好處

字符串常量對象可以共享的原因:字符串對象不可變

字符串常量對象共享的好處:節(jié)省內(nèi)存

String s1 = "atguigu";
String s2 = "atguigu";
System.out.println(s1 == s2);//這里只創(chuàng)建了一個字符串對象"atguigu"。

s2 = s2.replace("a","o");
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);

這里s1指向的"atguigu"和s2指向的"atguigu"是同一個。
如果無法保證"atguigu"對象不可變,那么當(dāng)s2將"a"替換為"o"之后,那么s1就會受到影響,這樣是不安全的。
但是,現(xiàn)在我們發(fā)現(xiàn)s1并未受到影響,也就是說,s1指向的"atguigu"對象并未被修改,而是基于"atguigu"重新復(fù)制了一個新對象"atguigu",然后替換成"otguigu"。

image-20221102104053826-17212813118184.png

2、hashCode方法

Object類有一個int hashCode()方法,該方法用于計算對象的哈希值。哈希值的作用就好比生活中的身份證號,用一串?dāng)?shù)字代表一個對象。哈希值的計算是有講究的,按照常規(guī)協(xié)定hashCode方法和equals方法要一起重寫,要求兩個“相等”的對象hashCode必須相同,如果兩個對象的哈希值不同,它倆調(diào)用equals方法也必須是false,但是如果兩個對象的哈希值相同,它倆調(diào)用equals方法卻不一定true。

字符串對象也重寫了hashCode方法,String類的hashCode值計算規(guī)則如下:

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

再好的哈希值計算規(guī)則也很難保證,兩個“不相等”對象的哈希值一定不同。例如:

String s3 = "Aa";//2112
String s4 = "BB";//2112

3、字符串常量池

字符串常量池是一個哈希表,它里面記錄了可以共享使用的字符串常量對象的地址。采用哈希表結(jié)構(gòu)的目的是為了提高性能,用空間換時間。字符串對象的地址散列存儲在哈希表中,雖然是散列存儲,但是因為可以使用字符串對象的hashCode值快速的計算存儲位置的下標(biāo),所以效率還是很高的。

String s1 = "hello";
String s2 = "hello";

當(dāng)給s1賦值"hello"時,根據(jù)"hello"的hashCode()值,計算出來index=[2],如果table[index]=table[2]=null,那就把"hello"對象的字符串的地址0x7534放到table[2]中。
    
當(dāng)給s2賦值"hello"時,根據(jù)"hello"的hashCode()值,計算出來index=[2],此時table[index]=table[2]!=null,那就直接把"hello"的內(nèi)存地址0x7534賦值給s2。
    
String s3 = "Aa";
String s4 = "BB";
當(dāng)給s3賦值"Aa"時,根據(jù)"Aa"的hashCode()值,計算出來index=[6],如果table[index]=table[6]=null,那就把"Aa"對象的字符串的地址0x8989放到table[6]中。
    
當(dāng)給s4賦值"BB"時,根據(jù)"BB"的hashCode()值,計算出來index=[6],此時table[index]=table[6]!=null,但是"BB"和"Aa"不一樣,那就直接把"BB"的內(nèi)存地址0x6666也放到table[6]中,相當(dāng)于table[6]中記錄了兩個字符串對象的地址,它們使用鏈表連接起來。    

4、哪些字符串對象地址放入字符串常量池?

需要共享的字符串地址記錄到字符串常量池的table表中,不需要共享的字符串對象其地址值不需要記錄到字符串常量池的table表中。除了以下2種,其他的都不放入字符串常量池:
(1)""直接的字符串 (備注:兩個""的字符串直接+,編譯器處理成一個""字符串)
(2)字符串對象.intern()結(jié)果
 
其他:
(1)直接new
(2)valueOf,copyValueOf等
(3)字符串對象拼接:concat拼接 以及 字符串變量 + 拼接
(4)toUpperCase,toLowerCase,substring,repalce等各種String方法得到的字符串
這些方式,本質(zhì)都是新new的。
package com.atguigu.string;

import org.junit.Test;

public class TestStringTable {
    @Test
    public void test1(){
        String s1 = "hello";
        String s2 = "hello";
        System.out.println(s1 == s2);
    }

    @Test
    public void test2(){
        String s1 = new String("hello");
        String s2 = new String("hello");
        String s3 = s1.intern();
        String s4 = s2.intern();
        System.out.println(s1 == s2);//false
        System.out.println(s3 == s4);//true
    }

    @Test
    public void test3(){
        String s1 = "hello";
        String s2 = "world";
        String s3 = "HELLOWORLD";

        String s4 = s1.concat(s2);
        String s5 = s1 + s2;
        String s6 = s3.toLowerCase();

        String s7 = "hello" + "world";
        String s8 = "helloworld";
        System.out.println(s4 == s8);//false
        System.out.println(s5 == s8);//false
        System.out.println(s6 == s8);//false
        System.out.println(s7 == s8);//true
    }
}

5、字符串對象和字符串常量池在哪里?

字符串常量池表:
  • JDK1.6:在方法區(qū)的永久代
  • JDK1.7之后:堆
字符串對象:
  • JDK1.7之前:需要共享的字符串對象存儲在方法區(qū)的永久代,然后把對象地址記錄到字符串常量池的table表中,不需要共享的字符串對象存儲在堆中,其地址值不需要記錄到字符串常量池的table表中。
  • JDK1.7之后:所有字符串對象都存儲在堆中。同樣需要共享的字符串地址記錄到字符串常量池的table表中,不需要共享的字符串對象其地址值不需要記錄到字符串常量池的table表中。

字符串的intern方法:

當(dāng)調(diào)用intern方法時,如果池已經(jīng)包含一個等于此String對象的字符串,則返回池中的字符串。否則,將此String對象添加到池中,并返回此String對象的引用。

package com.atguigu.string;

import org.junit.Test;

public class TestStringIntern {
    @Test
    public void test1(){
        String s1 = new String("hello");
        String s2 = s1.intern();
        System.out.println(s1 == s2);
        /*
        JDK8:false
        JDK6:false
         */
    }

    @Test
    public void test2(){
        String s1 = "he".concat("llo");
        String s2 = s1.intern();
        System.out.println(s1 == s2);
        /*
        JDK8:true
        JDK6:false
         */
    }
}

6、字符串的對象的個數(shù)(面試題)

String str1 = "hello";
String str2 = new String("hello");

//上面的代碼一共有幾個字符串對象。
//2個
String s1 = new String("hello");
String s2 = s1.intern();

//JDK1.6,2個
//JDK1.8,2個
String s = "a" + "b" + "c" +"d";
//1個,abcd
String s1 = "he".concat("llo");
String s2 = s1.intern();
//JDK1.6 4個
//JDK1.8 3個

9.6.4 字符串對象的內(nèi)存分析

就算不共享同一個字符串對象,字符串對象之間也會“盡量”共享同一個value數(shù)組。

package com.atguigu.string;

import org.junit.Test;

public class TestStringMemory {
    @Test
    public void test1(){
        String str1 = new String("hello");
        System.out.println(str1.hashCode());

        char[] arr = {'h','e','l','l','o'};
        String str2 = new String(arr);
    }
}

9.7 可變字符序列

9.7.1 String與可變字符序列的區(qū)別

因為String對象是不可變對象,雖然可以共享常量對象,但是對于頻繁字符串的修改和拼接操作,效率極低。因此,JDK又在java.lang包提供了可變字符序列StringBuilder和StringBuffer類型。

StringBuffer:老的,線程安全的(因為它的方法有synchronized修飾)

StringBuilder:線程不安全的

StringBuilder類底層是char[]數(shù)組存儲,默認(rèn)初始化長度16,每次增長是原長度 * 2 + 2

void expandCapacity(int minimumCapacity) { 
        int newCapacity = value.length * 2 + 2; 
        if (newCapacity - minimumCapacity < 0) 
            newCapacity = minimumCapacity; 
        if (newCapacity < 0) { 
            if (minimumCapacity < 0) 
                throw new OutOfMemoryError(); 
            newCapacity = Integer.MAX_VALUE; 
        } 
        value = Arrays.copyOf(value, newCapacity); 
    } 

JDK9之后,StringBuilder類底層是byte[]數(shù)組存儲,默認(rèn)長度和JDK8不同,有所改變

- new StringBuilder()源碼

//StringBuilder的父類構(gòu)造方法 
//StringBuilder(){super(16)}
AbstractStringBuilder(int capacity) {
    //如果為true,判斷為沒有中文字符
    if (COMPACT_STRINGS) {
        //數(shù)組value = new byte[16]
        value = new byte[capacity];
        //coder = 0
        coder = LATIN1;
    } else {
        //判斷有中文字符
        //value數(shù)組 = newBytesFor方法的返回值
        value = StringUTF16.newBytesFor(capacity);
        //coder = 1
        coder = UTF16;
    }
}
//返回字節(jié)數(shù)組
public static byte[] newBytesFor(int len) {
    if (len < 0) {
        throw new NegativeArraySizeException();
    }
    if (len > MAX_LENGTH) {
        throw new OutOfMemoryError("UTF16 String size is " + len +
                                   ", should be less than " + MAX_LENGTH);
    }
    //數(shù)組長度為 16 << 1  = 32
    return new byte[len << 1];
}

構(gòu)建StringBuilder對象,此時COMPACT_STRINGS變量為true,數(shù)組長度為16

- new StringBuilder("abc")源碼

AbstractStringBuilder(String str) {
    //獲取長度 lenght = 3
    int length = str.length();
    //數(shù)組容量 = 19
    int capacity = (length < Integer.MAX_VALUE - 16)
   ? length + 16 : Integer.MAX_VALUE;
    // initCoder = 0 無中文字符
    final byte initCoder = str.coder();
    coder = initCoder;
    //value數(shù)組創(chuàng)建后的長度為19
    value = (initCoder == LATIN1)
   ? new byte[capacity] : StringUTF16.newBytesFor(capacity);
    append(str);
}

- new StringBuilder("你好")源碼

AbstractStringBuilder(String str) {
    //獲取長度 lenght = 2
    int length = str.length();
    //數(shù)組容量 = 18
    int capacity = (length < Integer.MAX_VALUE - 16)? length + 16 : Integer.MAX_VALUE;
    // initCoder = 1 有中文字符
    final byte initCoder = str.coder();
    coder = initCoder;
    //value數(shù)組創(chuàng)建后的長度為36
    value = (initCoder == LATIN1)? new byte[capacity] : StringUTF16.newBytesFor(capacity);
        //(interCoder == LATIN1) 結(jié)果為false,執(zhí)行 StringUTF16.newBytesFor
    append(str);
}

9.7.2 StringBuilder、StringBuffer的API

常用的API,StringBuilder、StringBuffer的API是完全一致的

  1. StringBuffer append(xx):拼接,追加
  2. StringBuffer insert(int index, xx):在[index]位置插入xx
  3. StringBuffer delete(int start, int end):刪除[start,end)之間字符
  4. StringBuffer deleteCharAt(int index):刪除[index]位置字符
  5. void setCharAt(int index, xx):替換[index]位置字符
  6. StringBuffer reverse():反轉(zhuǎn)
  7. void setLength(int newLength) :設(shè)置當(dāng)前字符序列長度為newLength
  8. StringBuffer replace(int start, int end, String str):替換[start,end)范圍的字符序列為str
  9. int indexOf(String str):在當(dāng)前字符序列中查詢str的第一次出現(xiàn)下標(biāo) int indexOf(String str, int fromIndex):在當(dāng)前字符序列[fromIndex,最后]中查詢str的第一次出現(xiàn)下標(biāo)
  10. int lastIndexOf(String str):在當(dāng)前字符序列中查詢str的最后一次出現(xiàn)下標(biāo) int lastIndexOf(String str, int fromIndex):在當(dāng)前字符序列[fromIndex,最后]中查詢str的最后一次出現(xiàn)下標(biāo)
  11. String substring(int start):截取當(dāng)前字符序列[start,最后] String substring(int start, int end):截取當(dāng)前字符序列[start,end)
  12. String toString():返回此序列中數(shù)據(jù)的字符串表示形式
  13. void trimToSize():嘗試減少用于字符序列的存儲空間。如果緩沖區(qū)大于保存當(dāng)前字符序列所需的存儲空間,則將重新調(diào)整其大小,以便更好地利用存儲空間。
 @Test
    public void test6(){
        StringBuilder s = new StringBuilder("helloworld");
        s.setLength(30);
        System.out.println(s);
    }
    @Test
    public void test5(){
        StringBuilder s = new StringBuilder("helloworld");
        s.setCharAt(2, 'a');
        System.out.println(s);
    }
    
    
    @Test
    public void test4(){
        StringBuilder s = new StringBuilder("helloworld");
        s.reverse();
        System.out.println(s);
    }
    
    @Test
    public void test3(){
        StringBuilder s = new StringBuilder("helloworld");
        s.delete(1, 3);
        s.deleteCharAt(4);
        System.out.println(s);
    }
    
    
    @Test
    public void test2(){
        StringBuilder s = new StringBuilder("helloworld");
        s.insert(5, "java");
        s.insert(5, "chailinyan");
        System.out.println(s);
    }
    
    @Test
    public void test1(){
        StringBuilder s = new StringBuilder();
        s.append("hello").append(true).append('a').append(12).append("atguigu");
        System.out.println(s);
        System.out.println(s.length());
    }

9.7.3 效率測試

package com.atguigu.stringbuffer;

import org.junit.Test;

public class TestTime {

    @Test
    public void testString(){
        long start = System.currentTimeMillis();
        String s = new String("0");
        for(int i=1;i<=10000;i++){
            s += i;
        }
        long end = System.currentTimeMillis();
        System.out.println("String拼接+用時:"+(end-start));//367

        long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        System.out.println("String拼接+memory占用內(nèi)存: " + memory);//473081920字節(jié)
    }

    @Test
    public void testStringBuilder(){
        long start = System.currentTimeMillis();
        StringBuilder s = new StringBuilder("0");
        for(int i=1;i<=10000;i++){
            s.append(i);
        }
        long end = System.currentTimeMillis();
        System.out.println("StringBuilder拼接+用時:"+(end-start));//5
        long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        System.out.println("StringBuilder拼接+memory占用內(nèi)存: " + memory);//13435032
    }

    @Test
    public void testStringBuffer(){
        long start = System.currentTimeMillis();
        StringBuffer s = new StringBuffer("0");
        for(int i=1;i<=10000;i++){
            s.append(i);
        }
        long end = System.currentTimeMillis();
        System.out.println("StringBuffer拼接+用時:"+(end-start));//5
        long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
        System.out.println("StringBuffer拼接+memory占用內(nèi)存: " + memory);//13435032
    }
}

9.8 新特性:文本塊

在Java中,通常需要使用String類型表達HTML,XML,SQL或JSON等格式的字符串,在進行字符串賦值時需要進行轉(zhuǎn)義和連接操作,然后才能編譯該代碼,這種表達方式難以閱讀并且難以維護。而有了文本塊以后,用戶不需要轉(zhuǎn)義,Java能自動搞定。因此,**文本塊將提高Java程序的可讀性和可寫性。**

JDK 12引入了Raw String Literals特性,但在其發(fā)布之前就放棄了這個特性。這個JEP與引入多行字符串文字(文本塊)在意義上是類似的。Java 13中引入了文本塊(預(yù)覽特性),這個新特性跟Kotlin中的文本塊是類似的。

Java 14給文本塊引入了兩個新的轉(zhuǎn)義序列。一是可以使用新的\s轉(zhuǎn)義序列來表示一個空格;二是可以使用反斜杠“\”來避免在行尾插入換行字符,這樣可以很容易地在文本塊中將一個很長的行分解成多行來增加可讀性。

預(yù)覽的新特性文本塊在Java 15中被最終確定下來,Java 15之后我們就可以放心使用該文本塊了。

**舉例**

如有一段以下字符串:


  
      Hello, 尚硅谷
  

將其復(fù)制到Java的字符串中,會展示成以下內(nèi)容:

"\n" +
"    \n" +
"        Hello, 尚硅谷\n" +
"    \n" +
"\n";

即被自動進行了轉(zhuǎn)義,這樣的字符串看起來不是很直觀,在文本塊的新特性中,就可以使用以下語法了:

"""

  
      Hello, world
  

""";

使用"""作為文本塊的開始符和結(jié)束符,在其中就可以放置多行的字符串,不需要進行任何轉(zhuǎn)義。看起來就十分清爽了。

注意:

  • 開始分隔符由三個雙引號字符表示,后面只能跟零個或多個空格,最終以行終止符結(jié)束。
  • 文本塊內(nèi)容以開始分隔符的行終止符后的第一個字符開始,到結(jié)束分隔符的第一個雙引號之前的最后一個字符結(jié)束。

以下示例代碼是錯誤格式的文本塊:

String err1 = """""";//開始分隔符后沒有行終止符
String err2 = """  """;//開始分隔符后沒有行終止符
String err3 = """  abc
    """;  //開始分隔符后除了空格之外還有其他字符,然后才是行終止符

如果要表示空字符串需要以下示例代碼表示:

String emp1 = "";//推薦
String emp2 = """
   """;//第二種需要兩行,更麻煩了

案例:

public class TestStringBlock {
    public static void main(String[] args) {
        String htmlStr = """
            
              
                  Hello, world
              
            
            """;
        System.out.println(htmlStr);

        String story = """
           Elly said,"Maybe I was a bird in another life."
           Noah said,"If you're a bird, I'm a bird."
             """;
        System.out.println(story);

        String text = """
            \s\s人最寶貴的東西是生命,生命對人來說只有一次。\
            因此,人的一生應(yīng)當(dāng)這樣度過:當(dāng)一個人回首往事時,\
            不因虛度年華而悔恨,也不因碌碌無為而羞愧;\
            這樣,在他臨死的時候,能夠說,\
            我把整個生命和全部精力都獻給了人生最寶貴的事業(yè)\
            ——為人類的解放而奮斗。
            """;
        System.out.println(text);
    }
}