Java常用API(六)

9.6.3 字符串常量池

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

字符串常量对象可以共享的原因:字符串对象不可变

字符串常量对象共享的好处:节省内存

String s1 = "atguigu";
String s2 = "atguigu";
System.out.println(s1 == s2);//这里只创建了一个字符串对象"atguigu"。

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

这里s1指向的"atguigu"和s2指向的"atguigu"是同一个。
如果无法保证"atguigu"对象不可变,那么当s2将"a"替换为"o"之后,那么s1就会受到影响,这样是不安全的。
但是,现在我们发现s1并未受到影响,也就是说,s1指向的"atguigu"对象并未被修改,而是基于"atguigu"重新复制了一个新对象"atguigu",然后替换成"otguigu"。

image-20221102104053826-17212813118184.png

2、hashCode方法

Object类有一个int hashCode()方法,该方法用于计算对象的哈希值。哈希值的作用就好比生活中的身份证号,用一串数字代表一个对象。哈希值的计算是有讲究的,按照常规协定hashCode方法和equals方法要一起重写,要求两个“相等”的对象hashCode必须相同,如果两个对象的哈希值不同,它俩调用equals方法也必须是false,但是如果两个对象的哈希值相同,它俩调用equals方法却不一定true。

字符串对象也重写了hashCode方法,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;
    }

再好的哈希值计算规则也很难保证,两个“不相等”对象的哈希值一定不同。例如:

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

3、字符串常量池

字符串常量池是一个哈希表,它里面记录了可以共享使用的字符串常量对象的地址。采用哈希表结构的目的是为了提高性能,用空间换时间。字符串对象的地址散列存储在哈希表中,虽然是散列存储,但是因为可以使用字符串对象的hashCode值快速的计算存储位置的下标,所以效率还是很高的。

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

当给s1赋值"hello"时,根据"hello"的hashCode()值,计算出来index=[2],如果table[index]=table[2]=null,那就把"hello"对象的字符串的地址0x7534放到table[2]中。
    
当给s2赋值"hello"时,根据"hello"的hashCode()值,计算出来index=[2],此时table[index]=table[2]!=null,那就直接把"hello"的内存地址0x7534赋值给s2。
    
String s3 = "Aa";
String s4 = "BB";
当给s3赋值"Aa"时,根据"Aa"的hashCode()值,计算出来index=[6],如果table[index]=table[6]=null,那就把"Aa"对象的字符串的地址0x8989放到table[6]中。
    
当给s4赋值"BB"时,根据"BB"的hashCode()值,计算出来index=[6],此时table[index]=table[6]!=null,但是"BB"和"Aa"不一样,那就直接把"BB"的内存地址0x6666也放到table[6]中,相当于table[6]中记录了两个字符串对象的地址,它们使用链表连接起来。    

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

需要共享的字符串地址记录到字符串常量池的table表中,不需要共享的字符串对象其地址值不需要记录到字符串常量池的table表中。除了以下2种,其他的都不放入字符串常量池:
(1)""直接的字符串 (备注:两个""的字符串直接+,编译器处理成一个""字符串)
(2)字符串对象.intern()结果
 
其他:
(1)直接new
(2)valueOf,copyValueOf等
(3)字符串对象拼接:concat拼接 以及 字符串变量 + 拼接
(4)toUpperCase,toLowerCase,substring,repalce等各种String方法得到的字符串
这些方式,本质都是新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:在方法区的永久代
  • JDK1.7之后:堆
字符串对象:
  • JDK1.7之前:需要共享的字符串对象存储在方法区的永久代,然后把对象地址记录到字符串常量池的table表中,不需要共享的字符串对象存储在堆中,其地址值不需要记录到字符串常量池的table表中。
  • JDK1.7之后:所有字符串对象都存储在堆中。同样需要共享的字符串地址记录到字符串常量池的table表中,不需要共享的字符串对象其地址值不需要记录到字符串常量池的table表中。

字符串的intern方法:

当调用intern方法时,如果池已经包含一个等于此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、字符串的对象的个数(面试题)

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 字符串对象的内存分析

就算不共享同一个字符串对象,字符串对象之间也会“尽量”共享同一个value数组。

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与可变字符序列的区别

因为String对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低。因此,JDK又在java.lang包提供了可变字符序列StringBuilder和StringBuffer类型。

StringBuffer:老的,线程安全的(因为它的方法有synchronized修饰)

StringBuilder:线程不安全的

StringBuilder类底层是char[]数组存储,默认初始化长度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[]数组存储,默认长度和JDK8不同,有所改变

- new StringBuilder()源码

//StringBuilder的父类构造方法 
//StringBuilder(){super(16)}
AbstractStringBuilder(int capacity) {
    //如果为true,判断为没有中文字符
    if (COMPACT_STRINGS) {
        //数组value = new byte[16]
        value = new byte[capacity];
        //coder = 0
        coder = LATIN1;
    } else {
        //判断有中文字符
        //value数组 = newBytesFor方法的返回值
        value = StringUTF16.newBytesFor(capacity);
        //coder = 1
        coder = UTF16;
    }
}
//返回字节数组
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);
    }
    //数组长度为 16 << 1  = 32
    return new byte[len << 1];
}

构建StringBuilder对象,此时COMPACT_STRINGS变量为true,数组长度为16

- new StringBuilder("abc")源码

AbstractStringBuilder(String str) {
    //获取长度 lenght = 3
    int length = str.length();
    //数组容量 = 19
    int capacity = (length < Integer.MAX_VALUE - 16)
   ? length + 16 : Integer.MAX_VALUE;
    // initCoder = 0 无中文字符
    final byte initCoder = str.coder();
    coder = initCoder;
    //value数组创建后的长度为19
    value = (initCoder == LATIN1)
   ? new byte[capacity] : StringUTF16.newBytesFor(capacity);
    append(str);
}

- new StringBuilder("你好")源码

AbstractStringBuilder(String str) {
    //获取长度 lenght = 2
    int length = str.length();
    //数组容量 = 18
    int capacity = (length < Integer.MAX_VALUE - 16)? length + 16 : Integer.MAX_VALUE;
    // initCoder = 1 有中文字符
    final byte initCoder = str.coder();
    coder = initCoder;
    //value数组创建后的长度为36
    value = (initCoder == LATIN1)? new byte[capacity] : StringUTF16.newBytesFor(capacity);
        //(interCoder == LATIN1) 结果为false,执行 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():反转
  7. void setLength(int newLength) :设置当前字符序列长度为newLength
  8. StringBuffer replace(int start, int end, String str):替换[start,end)范围的字符序列为str
  9. int indexOf(String str):在当前字符序列中查询str的第一次出现下标 int indexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询str的第一次出现下标
  10. int lastIndexOf(String str):在当前字符序列中查询str的最后一次出现下标 int lastIndexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询str的最后一次出现下标
  11. String substring(int start):截取当前字符序列[start,最后] String substring(int start, int end):截取当前字符序列[start,end)
  12. String toString():返回此序列中数据的字符串表示形式
  13. void trimToSize():尝试减少用于字符序列的存储空间。如果缓冲区大于保存当前字符序列所需的存储空间,则将重新调整其大小,以便更好地利用存储空间。
 @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占用内存: " + memory);//473081920字节
    }

    @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占用内存: " + 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占用内存: " + memory);//13435032
    }
}

9.8 新特性:文本块

在Java中,通常需要使用String类型表达HTML,XML,SQL或JSON等格式的字符串,在进行字符串赋值时需要进行转义和连接操作,然后才能编译该代码,这种表达方式难以阅读并且难以维护。而有了文本块以后,用户不需要转义,Java能自动搞定。因此,**文本块将提高Java程序的可读性和可写性。**

JDK 12引入了Raw String Literals特性,但在其发布之前就放弃了这个特性。这个JEP与引入多行字符串文字(文本块)在意义上是类似的。Java 13中引入了文本块(预览特性),这个新特性跟Kotlin中的文本块是类似的。

Java 14给文本块引入了两个新的转义序列。一是可以使用新的\s转义序列来表示一个空格;二是可以使用反斜杠“\”来避免在行尾插入换行字符,这样可以很容易地在文本块中将一个很长的行分解成多行来增加可读性。

预览的新特性文本块在Java 15中被最终确定下来,Java 15之后我们就可以放心使用该文本块了。

**举例**

如有一段以下字符串:


  
      Hello, 尚硅谷
  

将其复制到Java的字符串中,会展示成以下内容:

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

即被自动进行了转义,这样的字符串看起来不是很直观,在文本块的新特性中,就可以使用以下语法了:

"""

  
      Hello, world
  

""";

使用"""作为文本块的开始符和结束符,在其中就可以放置多行的字符串,不需要进行任何转义。看起来就十分清爽了。

注意:

  • 开始分隔符由三个双引号字符表示,后面只能跟零个或多个空格,最终以行终止符结束。
  • 文本块内容以开始分隔符的行终止符后的第一个字符开始,到结束分隔符的第一个双引号之前的最后一个字符结束。

以下示例代码是错误格式的文本块:

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人最宝贵的东西是生命,生命对人来说只有一次。\
            因此,人的一生应当这样度过:当一个人回首往事时,\
            不因虚度年华而悔恨,也不因碌碌无为而羞愧;\
            这样,在他临死的时候,能够说,\
            我把整个生命和全部精力都献给了人生最宝贵的事业\
            ——为人类的解放而奋斗。
            """;
        System.out.println(text);
    }
}