概要
这一章的复习主要涵盖 String
的属性原理,String
常量池,反编译探究字符串拼接,以及 AbstractStringBuilder
的核心方法实现。这里整理的都是个人觉得比较重要重新认真分析的东西,不一定全部为基础,如果有不准确的地方,还望大佬从评论区指出,感激不尽。
文章中的所有的自定义的测试都在自己的 java-review
仓库中,本文对应的链接:
String
String 的创建
new String()
String s = new String("abc")
执行过程中创建了两个对象
- 第一个对象是
"abc"
字符串存储在 常量池 中 - 第二个对象在
JAVA Heap
中的String
对象
StringBuildler # append
编译器会自动优化,使用 StringBuilder 来拼接创建字符串
使用 StringBuilder
多字符串拼接,来创建字符串的时候只在堆中创建 String
对象,不会直接放到字符串常量池中,调用对象实例的 String # intern
方法
- 这个字符串不在字符串常量池中时,才会将这个在堆中的
Stirng
对象引用保存到常量池中并返回这个引用 - 常量池中存在这个
String
对象(直接创建的String
对象或者之前通过String # intern
保存过来的相同的字符串的引用),直接返回
空串 “” 和 null
""
代表的是一个字符长度为0
的字符串对象,而不是null
- 印证了 equals() 的比较原则,一个对象跟 null 进行比较的的结果永远是 null
1 | // test |
java 8-9 源码的变化
在 Java 8
中,String
内部使用 char[]
数组存储数据。
1 | // java 8 source |
在 Java 9
之后,String
类的实现改用 byte[]
数组存储字符串,同时使用 byte coder
来标识使用了哪种编码。
1 | // java 12 source |
String 常量池
何时将字符串保存到常量池中以及保存的格式(假设此时常量池中不存在 “123” 字符串):
String str = "123";
会直接在常量池中创建一个对象String str = new String("123");
堆中创建一个对象,常量池中创建一个对象
jdk 6 ~ 7 中的变化
从 jkd 6
到 jdk 7
中
- 将
String
常量池 从Perm
区移动到了Java Heap
区 String # intern
方法时,如果存在堆中的对象,会直接保存对象的引用,不会在常量池汇中重新创建对象。
String 和 常量池的关系
关于 str1 = new String("123")
和 str = "123"
的两种情况分析
str = new String("123");
分析- 在堆中创建一个
String
对象 - 字符串常量池中 没有 对应的字符串,就会将它的 引用放到常量池中
- 在堆中创建一个
str = "123";
分析- 常量池中没有这个
String
,直接在常量池中 创建这个字符串对象,并将引用返回 - 缓存池中有这个
String
,直接将引用返回,引用可能为- 常量池中的对象引用
- 常量池中保存的 指向堆中
String
对象的引用
- 常量池中没有这个
关于 StringBuilder
创建字符串的情况分析
从反编译的字节码中可以看到,使用 StringBuilder
来拼接 String
对象引用的时候,不会在常量池中创建对象,调用 String # inter
方法的时候就是将自身放进了常量池中,需要注意的是
StringBuilder
创建字符串测试测试
1 | // test |
StringBuilder
是否在常量池中创建对象,受其拼接的字符换长度影响
- 只有一个字符串进行拼接的时候,就会常量池中创建一个对象,堆中创建一个
String
对象,这时候调用str.intern()
的时候返回的就是常量池创建的那个对象,所以不同str != str.intern()
new StringBuilder("123").toString();
new StringBuilder().append("123").toString();
- 多个字符串拼接,不会在常量池中创建对象,这时候调用
str.intern()
的时候,就会将自身的引用放到常量池中并返回,所以相同str == str.intern()
new StringBuilder("123").append("456").toString();
new StringBuilder().appned("123").append("456").toString();
String intern()
1 | // java 12 source |
String # intern
方法的作用会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中并将引用返回- 任何
String
对象的可以调用String # inter
方法,只是对于str = new String("")
和str = ""
没有意义,因为已经被放入了常量池中
几个重要的点
+
拼接String
对象引用 来得到的String
不会直接放到到常量池中!String
对象引用可以通过String # intern
方法将String
放到常量池中jdk 7
以后,常量池中保存的为String
的引用,而非直接在常量池中生成一个对象- 如果使用了
StringBuilder
来创建字符串- 只有一个字符串的时候的时候 (
new StringBuilder("123")
或者new StringBuilder().append("123").toString()
),就会常量池中创建一个对象,堆中创建一个String
对象,这时候调用str.intern()
的时候返回的就是常量池创建的那个对象,所以不同 - 有多个
appned("")
的时候,不会在常量池中创建对象,这时候调用str.intern()
的时候,就会将自身的引用放到常量池中并返回,所以相同
- 只有一个字符串的时候的时候 (
1 | // test source |
jdk 6
中的常量池是放在 Perm
区中的,Perm
区和正常的 JAVA Heap
区域是完全分开的,所以拿一个 JAVA Heap
区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的,即使调用 String # intern
方法也是没有任何关系的
使用引号声明的字符串都是会直接在字符串常量池中生成,而 new
出来的 String
对象是放在 JAVA Heap
区域
String 的不变性
不变性的优点
从上面的 Stirng
的属性中可以知道 hash
不变性的分析
value
数组被声明为private final
,将数组引用锁定,并且String
中没有对value
数组进行修改的地方,所以String
为不可变对象String
只要发生了修改,就会new
一个新的String
对象,这里以String # toLowerCase
方法为例,这个方法的返回值重新new
了一个新的对象作为返回值
1 | // java 12 source |
重载 “+” 和 “+=”
java
不允许操作符重载,但是为 String
对象重载了 +
以及 +=
操作符。
String 类型提升
就如同前面的 算数运算 会自动将运算数的类型提高为 运算数的最高类型 一样,只要根据结合律执行遇到了 String
就换转换为 String
类型,在这个环境中,对象引用会自动调用 String toString()
方法返回一个字符串用于拼接。
表达式会根据操作符的结合律来进行执行,当遇到第一个 String
类型,就将当前的类型直接提升到 String
,即当前变为 String
环境
在
String
环境中的对象引用都会调用被引用对象的Object # toString
来得到一个String
用于拼接字符串在
str += obj + a * b;
中- 先执行
a * b
此时的结果的为int
类型 - 执行
obj + ret
,此时obj
为对象引用
而a * b
的结果为int
,非String
环境报错
- 先执行
1 | // test source |
“+” 字符串拼接分析
拼接 字面量字符串 和 String
对象引用 在反编译的结果中,并不是完全相同
- 连续的字面量字符串 会如同反编译代码的
33
行,将连续的字面量字符串使用ldc
一次性完成加载 - 存在
String
对象引用的拼接都会先new
一个StringBuilder
对象并完成初始化,使用StringBuilder # append
来拼接单个String
或者是连续的字面量字符串 - 需要注意的是,编译器会为 每个拼字符串式 创建一个
StringBuilder
对象,如果将+=
操作符用于了for
循环中就会每经过一次循环体,就会创建新的StringBuilder
对象,
所以对于 for
循环拼字符串就不能直接使用 “+=”,因为这样还是需要使用 StringBuilder
来代替 String
+ +/+=
测试代码及反编译字节码
1 | String a = "who"; |
测试什么时候创建新的 StringBuilder 对象
这里用来测试字符串的拼接那种情况会触发创建 StringBuilder
对象,以及连续的 str += ""
调用是使用同一个 StringBuilder
对象? 还是会每条语句都会创建新的 StringBuilder
对象?
- 对于纯字面量的字符串赋值表达式 (
=
赋值),不会创建StringBuilder
对象,字面量字符串直接装载,String
会创建String
对象 - 对于存在了
String
对象引用的表达式中(如str += ""
, 或者str = str1 + ""
) 都会创建新的StringBuilde
r 对象,一条表达式一个StringBuilder
对象,也就证明了为什么for
循环不推荐使用str += ""
来进行延长字符串
1 | // test source |
AbstractStringBuilder
String
是不可修改对象,其内部也没有进行修改的方法,而 AbstractStringBuilder
就是可以修改的 String
的抽象类,他的两个导出类
StringBuilder
-> 线程不安全StringBuffer
-> 线程安全,方法使用了synchronized
加锁
关于 StringBuilder
来创建的字符串和常量池的关系在上面的 String Pool
中有详细的说明
appned() 方法
StringBuilder # append
方法只是调用父类方法,父类 AbstractStringBuilder
才是核心,这里面的几个核心方法
String # void getBytes(byte dst[], int dstBegin, byte coder)
这个方法的作用没有权限修饰符,所以权限为default
,所以 无法使用该方法AbstractStringBuilder # ensureCapacityInternal(int minimumCapacity)
确保内部容量(扩容方法) 的作用就是将数组的长度更新,使用的是Arrays # byte[] copyOf(byte[] original, int newLength)
这个方法的作用就是为了 截断 或者 扩展 数组。
1 | // java 12 source |
- 通过返回
this
实现了串行调用
1 | // java 12 source |
源码分析
1 | // java 12 source |
insert() 方法
AbstractStringBuilder # insert
方法比起 AbstractStringBuilder # append
方法就是多了一步: 将插入位置后的字符都向后移动 n
位
1 | // java 12 source |