抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

摘要:本文学习了和字符串有关的类库。

环境

Windows 10 企业版 LTSC 21H2
Java 1.8

1 String

1.1 不可变

String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。

String类其实是通过char数组来保存字符串的。

所有对String类的操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。

String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何改变都会生成新的对象。

下面代码中对String的改动并不会影响到原有的String字符串:

java
1
2
3
4
5
6
7
8
9
10
11
public static void change(String str) {
str = str + "def";
}
public static void main(String[] args) {
String a = "abc";
String b = new String("abc");
change(a);
change(b);
System.out.println("a >>> " + a);// a >>> abc
System.out.println("b >>> " + b);// b >>> abc
}

1.2 常量池

JVM为了提高性能和减少内存的开销,每当创建字符串常量时,JVM会首先检查字符串常量池。

如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。

如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,同时返回对该实例的引用。

示例:

java
1
2
String a = "abc";
String b = new String("abc");

说明:

  • a保存的是常量池里abc的地址,这是在编译时就能确定的。
  • b保存的是堆内存里abc的地址,因为字符串都是保存在常量池里的,所以堆内存里保存的也是常量池里abc的地址。因为在编译时是不确定的,所以b保存的地址值是在运行时确定的。

示例:

java
1
2
3
4
5
6
7
8
9
public void test() {
String a = "abc";
String b = "abc";
String c = new String("abc");
String d = new String("abc");
System.out.println(a == b);// true
System.out.println(a == c);// false
System.out.println(c == d);// false
}

说明:

  • a和b保存的是常量池里abc的地址,常量池里只有一个abc的地址,所以a == b判断返回true。
  • c和d保存的是堆内存里abc的地址,通过new创建的对象会有独立的堆内存的地址,所以a == cc == d判断返回false。

示例:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void test() {
String a = "abc";
String b = "def";
String c = a + b;
String d = a + b;
String e = "abc" + "def";
String f = "abcdef";
String g = "abc" + new String("def");
System.out.println(c == d);// false
System.out.println(c == e);// false
System.out.println(c == f);// false
System.out.println(c == g);// false
System.out.println(e == f);// true
System.out.println(f == g);// false
}

说明:

  • 因为a + b拼接的是变量,在编译期不能确定结果,需要在运行期确定,所以c和d保存的是堆内存里的地址,并且堆内存里的地址都是独立的,所以c == d判断返回false。
  • 因为"abc" + "def"拼接的是字符串,在编译期能确定结果,所以e保存的是常量池里的地址,并且e == f判断返回true。
  • 因为g拼接的是变量,保存的是堆内存里的地址,所以f == g判断返回false。

示例:

java
1
2
3
4
5
6
7
8
9
public void test() {
final String a = "abc";
String b = "abc";
String c = a + "def";
String d = b + "def";
String e = "abcdef";
System.out.println(c == e);// true
System.out.println(d == e);// false
}

说明:

  • 因为a是被final修饰的常量,对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝,存储到常量池中或嵌入到它的字节码流中。
  • 因为a是常量,所以a + "def"在编译期间可以确定,c保存的是常量池里的地址,执行c == e判断为true。
  • 因为b是变量,所以b + "def"在编译期间不能确定,d保存的是堆内存里的地址,执行d == e判断为false。

1.3 拼接

编译器每次碰到+拼接的时候,都会创建StringBuilder对象并调用append()方法进行拼接,再调用toString()方法生成新字符串。如果代码中有很多的+操作就会创建很多StringBuilder对象,这种方式对内存是一种浪费。

示例:

java
1
2
3
4
5
6
7
8
9
public void test() {
String a = "abc";
String b = "def";
String c = "abc" + "def";
String d = a + b;
String e = "abcdef";
System.out.println(c == e);// true
System.out.println(d == e);// false
}

字符串对字面量和变量的拼接操作是有区别的:

  • 字符串对字面量的拼接操作"abc" + "def"是在编译期执行的,会将拼接字符串放到常量池中,返回的是常量池的地址。
  • 字符串对变量的拼接操作a + b是在运行期执行的,会将拼接字符串放到堆内存中,返回的是堆内存的地址。

1.4 关于intern()方法

使用intern()方法会从字符串常量池中查询与当前字符串相同的字符串,如果存在则返回常量池的地址,如果不存在则在常量池中创建字符串,并更新当前对象保存常量池的地址。

1.4.1 示例一

示例:

java
1
2
3
4
5
6
public void test(){
String a = new String("ab");
a.intern();
String b = "ab";
System.out.println(a == b);// false
}

说明:

  • 创建a后,在堆内存和常量池创建"ab"字符串,此时a保存的是堆内存的地址。
  • 执行a.intern()方法后,在常量池找到"ab"字符串,不会更新a的引用,所以此时a保存的是堆内存的地址。
  • 创建b后,在常量池找到"ab"字符串,此时b保存的是常量池的地址。

1.4.2 示例二

示例:

java
1
2
3
4
5
6
public void test(){
String a = new String("a") + new String("b");
a.intern();
String b = "ab";
System.out.println(a == b);// true
}

说明:

  • 创建a后,在堆内存和常量池创建"a""b"字符串,并且只在堆内存创建拼接的"ab"字符串,此时a保存的是堆内存的地址。
  • 执行a.intern()方法后,在常量池找不到"ab"字符串,在常量池创建字符串,并且更新a的引用,所以此时a保存的是常量池的地址。
  • 创建b后,在常量池找到"ab"字符串,此时b保存的是常量池的地址。

1.4.3 示例三

示例:

java
1
2
3
4
5
6
public void test(){
String a = new String("a") + new String("b");
String b = "ab";
a.intern();
System.out.println(a == b);// false
}

说明:

  • 创建a后,在堆内存和常量池创建"a""b"字符串,并且只在堆内存创建拼接的"ab"字符串,此时a保存的是堆内存的地址。
  • 创建b后,在常量池找不到"ab"字符串,在常量池创建字符串,此时b保存的是常量池的地址。
  • 执行a.intern()方法后,在常量池找到"ab"字符串,不会更新a的引用,所以此时a保存的是堆内存的地址。

1.4.4 示例四

示例:

java
1
2
3
4
5
6
public void test(){
String a = new String("a") + new String("b");
String b = "ab";
a = a.intern();
System.out.println(a == b);// true
}

说明:

  • 创建a后,在堆内存和常量池创建"a""b"字符串,并且只在堆内存创建拼接的"ab"字符串,此时a保存的是堆内存的地址。
  • 创建b后,在常量池找不到"ab"字符串,在常量池创建字符串,此时b保存的是常量池的地址。
  • 执行a.intern()方法后,在常量池找到"ab"字符串,并且更新a的引用,所以此时a保存的是常量池的地址。

2 StringBuilder

构造方法:

java
1
2
3
4
// 创建一个长度为默认16的char类型的数组
public StringBuilder();
// 创建一个指定长度的char类型的数组
public StringBuilder(int capacity);

常用方法:

java
1
2
3
4
5
6
7
8
// 追加内容
public StringBuilder append(String str);
// 删除指定位置的内容
public StringBuilder delete(int start, int end);
// 把指定位置的字符串替换为传入的字符串
public StringBuilder replace(int start, int end, String str);
// 在指定位置插入传入的字符串
public StringBuilder insert(int offset, String str);

3 StringBuffer

构造方法:

java
1
2
3
4
// 创建一个长度为默认16的char类型的数组
public StringBuffer();
// 创建一个指定长度的char类型的数组
public StringBuffer(int capacity);

常用方法:

java
1
2
3
4
5
6
7
8
// 追加内容
public synchronized StringBuffer append(String str);
// 删除指定位置的内容
public synchronized StringBuffer delete(int start, int end);
// 把指定位置的字符串替换为传入的字符串
public synchronized StringBuffer replace(int start, int end, String str);
// 在指定位置插入传入的字符串
public synchronized StringBuffer insert(int offset, String str);

4 比较

4.1 可变与不可变

String是不可变字符串对象。

StringBuilder和StringBuffer是可变字符串对象,其内部的字符数组长度可变。

4.2 是否多线程安全

String中的对象是不可变的,也就可以理解为常量,显然线程安全。

StringBuffer中的方法大都采用了synchronized关键字进行修饰,是线程安全的。

StringBuilder没有synchronized关键字修饰,是非线程安全的。

4.3 执行效率

执行效率从高到低排序是StringBuilder > StringBuffer > String顺序。

应当根据不同的情况来进行选择使用:

  • 当字符串相加操作或者改动较少的情况下,建议使用String这种形式。
  • 当字符串相加操作较多的情况下,如果不需要保证线程安全,建议使用StringBuilder这种形式,执行效率会高一些。
  • 当字符串相加操作较多的情况下,如果需要保证线程安全,建议使用StringBuffer这种形式。

评论