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)