摘要:本文学习了和字符串有关的类库。
环境
Windows 10 企业版 LTSC 21H2
Java 1.8
1 String
1.1 不可变
String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。
String类其实是通过char数组来保存字符串的。
所有对String类的操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。
String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何改变都会生成新的对象。
下面代码中对String的改动并不会影响到原有的String字符串:
1 | public static void change(String str) { |
1.2 常量池
JVM为了提高性能和减少内存的开销,每当创建字符串常量时,JVM会首先检查字符串常量池。
如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。
如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,同时返回对该实例的引用。
示例:
1 | String a = "abc"; |
说明:
- a保存的是常量池里abc的地址,这是在编译时就能确定的。
- b保存的是堆内存里abc的地址,因为字符串都是保存在常量池里的,所以堆内存里保存的也是常量池里abc的地址。因为在编译时是不确定的,所以b保存的地址值是在运行时确定的。
示例:
1 | public void test() { |
说明:
- a和b保存的是常量池里abc的地址,常量池里只有一个abc的地址,所以
a == b
判断返回true。 - c和d保存的是堆内存里abc的地址,通过new创建的对象会有独立的堆内存的地址,所以
a == c
和c == d
判断返回false。
示例:
1 | public void test() { |
说明:
- 因为
a + b
拼接的是变量,在编译期不能确定结果,需要在运行期确定,所以c和d保存的是堆内存里的地址,并且堆内存里的地址都是独立的,所以c == d
判断返回false。 - 因为
"abc" + "def"
拼接的是字符串,在编译期能确定结果,所以e保存的是常量池里的地址,并且e == f
判断返回true。 - 因为g拼接的是变量,保存的是堆内存里的地址,所以
f == g
判断返回false。
示例:
1 | public void test() { |
说明:
- 因为a是被final修饰的常量,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝,存储到常量池中或嵌入到它的字节码流中。
- 因为a是常量,所以
a + "def"
在编译期间可以确定,c保存的是常量池里的地址,执行c == e
判断为true。 - 因为b是变量,所以
b + "def"
在编译期间不能确定,d保存的是堆内存里的地址,执行d == e
判断为false。
1.3 拼接
编译器每次碰到+
拼接的时候,都会创建StringBuilder对象并调用append()
方法进行拼接,再调用toString()
方法生成新字符串。如果代码中有很多的+
操作就会创建很多StringBuilder对象,这种方式对内存是一种浪费。
示例:
1 | public void test() { |
字符串对字面量和变量的拼接操作是有区别的:
- 字符串对字面量的拼接操作
"abc" + "def"
是在编译期执行的,会将拼接字符串放到常量池中,返回的是常量池的地址。 - 字符串对变量的拼接操作
a + b
是在运行期执行的,会将拼接字符串放到堆内存中,返回的是堆内存的地址。
1.4 关于intern()方法
使用intern()
方法会从字符串常量池中查询与当前字符串相同的字符串,如果存在则返回常量池的地址,如果不存在则在常量池中创建字符串,并更新当前对象保存常量池的地址。
1.4.1 示例一
示例:
1 | public void test(){ |
说明:
- 创建a后,在堆内存和常量池创建
"ab"
字符串,此时a保存的是堆内存的地址。 - 执行
a.intern()
方法后,在常量池找到"ab"
字符串,不会更新a的引用,所以此时a保存的是堆内存的地址。 - 创建b后,在常量池找到
"ab"
字符串,此时b保存的是常量池的地址。
1.4.2 示例二
示例:
1 | public void test(){ |
说明:
- 创建a后,在堆内存和常量池创建
"a"
和"b"
字符串,并且只在堆内存创建拼接的"ab"
字符串,此时a保存的是堆内存的地址。 - 执行
a.intern()
方法后,在常量池找不到"ab"
字符串,在常量池创建字符串,并且更新a的引用,所以此时a保存的是常量池的地址。 - 创建b后,在常量池找到
"ab"
字符串,此时b保存的是常量池的地址。
1.4.3 示例三
示例:
1 | public void test(){ |
说明:
- 创建a后,在堆内存和常量池创建
"a"
和"b"
字符串,并且只在堆内存创建拼接的"ab"
字符串,此时a保存的是堆内存的地址。 - 创建b后,在常量池找不到
"ab"
字符串,在常量池创建字符串,此时b保存的是常量池的地址。 - 执行
a.intern()
方法后,在常量池找到"ab"
字符串,不会更新a的引用,所以此时a保存的是堆内存的地址。
1.4.4 示例四
示例:
1 | public void test(){ |
说明:
- 创建a后,在堆内存和常量池创建
"a"
和"b"
字符串,并且只在堆内存创建拼接的"ab"
字符串,此时a保存的是堆内存的地址。 - 创建b后,在常量池找不到
"ab"
字符串,在常量池创建字符串,此时b保存的是常量池的地址。 - 执行
a.intern()
方法后,在常量池找到"ab"
字符串,并且更新a的引用,所以此时a保存的是常量池的地址。
2 StringBuilder
构造方法:
1 | // 创建一个长度为默认16的char类型的数组 |
常用方法:
1 | // 追加内容 |
3 StringBuffer
构造方法:
1 | // 创建一个长度为默认16的char类型的数组 |
常用方法:
1 | // 追加内容 |
4 比较
4.1 可变与不可变
String是不可变字符串对象。
StringBuilder和StringBuffer是可变字符串对象,其内部的字符数组长度可变。
4.2 是否多线程安全
String中的对象是不可变的,也就可以理解为常量,显然线程安全。
StringBuffer中的方法大都采用了synchronized关键字进行修饰,是线程安全的。
StringBuilder没有synchronized关键字修饰,是非线程安全的。
4.3 执行效率
执行效率从高到低排序是StringBuilder > StringBuffer > String
顺序。
应当根据不同的情况来进行选择使用:
- 当字符串相加操作或者改动较少的情况下,建议使用String这种形式。
- 当字符串相加操作较多的情况下,如果不需要保证线程安全,建议使用StringBuilder这种形式,执行效率会高一些。
- 当字符串相加操作较多的情况下,如果需要保证线程安全,建议使用StringBuffer这种形式。
条