StringTable(三)

简介: StringTable(三)

StringTable(二):https://developer.aliyun.com/article/1535761







1.8 JDK 9 之后的改变

可以看到,本质上就是根据 StringBuilder 维护的 char[] 创建了新的 String 对象

public final class StringBuilder 
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {
    
    // 从 AbstractStringBuilder 继承的属性,方便阅读加在此处
    char[] value;
    
    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }    
}

StringBuilder 的 toString() 方法又是怎么实现的呢?

String x = "b";
String s = "a" + x;

String x = "b";
String s = new StringBuilder().append("a").append(x).toString();

翻译成人能读懂的就是

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // String b
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5                  // String a
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: aload_1
        16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: astore_2
        23: return

主方法

可以看到常量池中并没有 ab 字面量

Constant pool:
   #1 = Methodref          #9.#26         // java/lang/Object."<init>":()V
   #2 = String             #27            // b
   #3 = Class              #28            // java/lang/StringBuilder
   #4 = Methodref          #3.#26         // java/lang/StringBuilder."<init>":()V
   #5 = String             #29            // a
   ...

常量池

String x = "b";
String s = "a" + x;

那么,什么是真正的【拼接】操作呢?看一下例3 反编译后的结果

可以看到,还是没有真正的【拼接】操作发生,final 意味着 x 的值不可改变,因此其它引用 x 的地方都可以安全地被替换为 "b",而不用担心 x 被改变,从源码编译为字节码时,javac 就也进行了优化,把所有出现 x 的地方都替换成为了 "b"

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: ldc           #2                  // String b   final b
         2: astore_1
         3: ldc           #3                  // String ab
         5: astore_2
         6: return
         ...

主方法

Constant pool:
   #1 = Methodref          #5.#22         // java/lang/Object."<init>":()V
   #2 = String             #23            // b
   #3 = String             #24            // ab
   ...

常量池

final String x = "b";
String s = "a" + x;

例2

可以看到,其实并没有真正的【拼接】操作发生,从源码编译为字节码时,javac 就已经把 "a" 和 "b" 串在一起了,这是一种编译期的优化处理

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // String ab
         2: astore_1
         3: return
         ...

主方法

Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = String             #21            // ab
   ...

常量池

String s = "a" + "b";

例1

有同学会问,例1与例2与例3 不同吗?还别说,真就不同,其中例1 与例2 原理是一样的,例3 与例4 原理是一样的,反编译一下

String s = "a" + 1;

例4

String x = "b";
String s = "a" + x;

例3

final String x = "b";
String s = "a" + x;

例2

String s = "a" + "b";

例1

最后还可以通过 + 运算符将两个字符串(其中一个也可以是其它类型)拼接为一个新字符串,例如

1.7 拼接创建

具体原理我们下一个章节再讲

true
true

运行结果

public class TestString1 {
    public static void main(String[] args) {
        String s1 = "abc"; // 字符串对象 "abc"
        String s2 = "abc"; // 字符串对象 "abc"
        TestString2.main(new String[]{s1, s2});
    }
}

public class TestString2 {
    public static void main(String[] args) { // args[0] "abc", args[1] "abc"
        String s1 = "a";
        String s2 = "abc";
        System.out.println(args[0] == s2);
        System.out.println(args[1] == s2);
    }
}

我们来做个实验,把刚才的代码做个改写

这时候【字面量】是两份,而【字符串对象】会有几个呢?

可以看到在这个类中,"abc" 对应的常量池的编号是 #3,与 TestString1 中的已经不同

Constant pool:
   #1 = Methodref          #5.#22         // java/lang/Object."<init>":()V
   #2 = String             #23            // a
   #3 = String             #24            // abc

对应的常量池

public class TestString2 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "abc";
    }
}

例如,另一个类中

如果是不同类中的 "abc" 呢?【类文件常量池】包括【运行时常量池】都是以类为单位的

可以看到 "abc" 这个字面量虽然出现了 2 次,但实际上都是对应着常量池中 #2 这个地址

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: ldc           #2                  // String abc
         2: astore_1
         3: ldc           #2                  // String abc
         5: astore_2
         6: return
         ...

对应的字节码为

Constant pool:
   #1 = Methodref          #25.#48        // java/lang/Object."<init>":()V
   #2 = String             #49            // abc
   ...

常量池为

public class TestString1 {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "abc";
    }
}

同一个类中的值相同字面量,其实只有一份

不重复

执行到断点3 处,这时新创建了 "2" 对应的字符串对象,个数为 2413

执行到断点2 处,这时新创建了 "1" 对应的字符串对象,个数为 2412

刚开始在断点1 处,其它类中创建的字符串对象有 2411 个

可以给每行语句加上断点,然后用 idea 的 debug 界面中的 memory 工具来查看字符串对象的数量

System.out.println();
System.out.println("1"); // 断点1 2411
System.out.println("2"); // 断点2 2412
System.out.println("3"); // 断点3

例如有如下代码

如何验证呢?

当第一次用到 "abc"  字面量时(也就是执行到 ldc #2 时) ,才会创建对应的字符串对象

懒加载

ldc #2 就是到运行时常量池中找到 #2 的内存地址,找到 "abc"  这个字面量,再根据它创建一个 String 对象。

0: ldc           #2                  // String abc
2: astore_1
3: return

将来 main 方法被调用时,就会执行里面的字节码指令

public static void main(java.lang.String[]); // 字节码指令
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // String abc
         2: astore_1
         3: return
         ...

再看一下 class 中 main 方法的字节码

当 class 完成类加载之后,"abc" 这个字面量被存储于【运行时常量池】(归属于方法区)中,其中 #1 #2 都会被翻译为运行时真正的内存地址

Constant pool: // 常量池
   #1 = Methodref          #19.#41        // java/lang/Object."<init>":()V
   #2 = String             #42            // abc
   ...

在上面的 java 代码被编译为 class 文件后,"abc"  存储于【类文件常量池】中

要理解从字面量变成字符串对象的过程,需要从字节码的角度来分析

严格地说,字面量在代码运行到它所在语句之前,它还不是字符串对象

非对象

"abc"  被叫做字符串字面量(英文 Literal),但恰恰是这种方式其实奥妙最多,我总结了三点:非对象、懒加载、不重复。来逐一看一下

以上四种创建方式,大家用的实际上相对少一点,最熟悉的是这种字面量的方式:

1.6 字面量创建

内存结构如下

这种最为简单,但要注意是两个字符串对象引用同一个 char[] 对象

转换过程如图所示

有时候我们还需要用两个 char 表示一个字符,比如 😂 这个笑哭的字符,它用 unicode 编码表示为 0x1F602,存储范围已经超过了 char 能表示的最大值 0xFFFF,因此需要使用 int[] 来构造这样的字符串,如下

1.4 int[] 数组创建

其实 java 中的 char 字符都是以 unicode 编码的,从外界不同的编码(如 gbk,utf-8)传过来的 byte[] 最终到 java 中的 char 都统一了

其中三个 byte 0xE5,0xBC 和 0xA0 被转换成了一个 char 0x5F20(汉字【张】)

例2,按 utf-8 字符集转换

其中两个 byte 0xD5 和 0xC5 被转换成了一个 char 0x5F20(汉字【张】)

这时

例1,按 gbk 字符集转换

看到上幅图有同学会说,对于 byte[] 转换为 char[],97 还是对应 97,98 还是对应 98,99 还是对应 99 啊,看不出 byte[] 和 char[] 的任何区别啊?你要知道,首先他们的大小不一样,其次上面的 char[] 中的 97(a),98(b),99(c) 都属于拉丁字符集,如果用到其它字符集,那么结果就不一样了,看下面的例子

其中 byte[] 和 char [] 的结构如下

这时 byte[] 会在构造时被转换为 char[]

它的内部结构其实也是

  • 从网络(例如一个浏览器的 http 请求)传递过来的字节数据
  • 也可以是从 I/O(例如从一个文本文件)读取到的数据

其中 new byte[]{97, 98, 99} 就可以是

例如

答案是,从网络传递过来的数据,或是 I/O 读取到的数据,都有从 byte[] 转为字符串的需求

有同学会问,什么时候会根据 byte[] 数组来创建字符串呢?

1.3 byte[] 数组创建

其中 97 其实就是 'a' ,98 其实就是 'b' ,99 其实就是 'c'

它的内部结构如下(1.8)


目录
相关文章
|
3月前
|
机器学习/深度学习 存储 Java
StringTable(二)
StringTable(二)
25 1
|
3月前
|
Java 程序员
StringTable(一)
StringTable(一)
29 1
|
存储 Java API
jvm之StringTable解读(二)
jvm之StringTable解读(二)
|
4月前
|
存储 缓存 算法
对象和数组并不是都是在堆上分配内存的
对象和数组并不是都是在堆上分配内存的
40 0
|
Java C++
jvm之StringTable解读(三)
jvm之StringTable解读(三)
|
存储 缓存 Oracle
|
存储 缓存 Java
JVM - 深入剖析字符串常量池
JVM - 深入剖析字符串常量池
115 0
|
Java C++
StringTable(2)
StringTable
72 0
|
存储 机器学习/深度学习 Java
StringTable(3)
StringTable
78 0