网上看面试题时经常看到各种字符串比较的问题,有时看着答案也不知道为什么。于是今天花了一点时间对此做了一下深入的学习,在此记录一下。

创建字符串时需要注意的规则

这里列的规则是我结合JDK里的文档和《Java-String.intern的深入研究》、《几张图轻松理解String.intern()》这两篇文章,对于理解下面的实例中我认为比较关键的几点,可能有些理解不正确。

1、通过new String(String original)会有涉及到两个对象。
例如 String str = new String("a")语句,会先将构造函数里的参数original指向在字符串常量池(简称SCP),如果常量池中不存在,则会在常量池中生成字符串a,再在堆(HEAP)中生成变量str;

2、如果一个字符串str是由多个常量字符串通过**+**拼接的,则字符串str会直接生成或指向在字符串常量池中。

情况一:

1
String str = "a" + "b";

情况二:

1
2
String b = "b";
String str = "a" + b;

情况三:

1
2
final String b = "b";
String str = "a" + b;

在上面的三种情况中,第一种和第三种情况的str都是由常量字符串直接拼接的,所以str会直接指向字符串常量池;而情况二中由于存在局部变量b,编译器将会通过StringBuilder.append()方法拼接字符串a和变量b后,最终再通过StringBuilder.toString()方法得到strstr会在堆中生成。

3、JDK 1.7后,在执行 String.intern()方法时,虚拟机会去字符串常量池检查是否已存在该字符串,如果存在则会直接引用常量池中该字符串的地址作为返回结果的引用地址;如果不存在,则会在常量池中生成一个对在原字符串(位于堆中)的引用作为,而不是像 JDK 1.6之前仍将原字符串拷贝到常量池中。

实例

实例1

1
2
3
4
5
6
7
8
@Test
public void test1() {
String c = "ab"; //SCP
String i = "a" + "b"; //SCP
String j = i.intern(); //SCP
System.out.println(i == j);
System.out.println(c == j);
}

String c = "ab"将直接在字符串常量池生成字符串ab;由于i是由两个字符串常量ab直接拼接而成,所以i也会指向字符串常量池;由于i.intern()得到的字符串在常量池中已存在,所以j也指向常量池。因此cij指向的同一个地址。因此输出结果为:

1
2
3
true
true
true

实例2

1
2
3
4
5
6
7
8
9
@Test
public void test2() {
String c = "ab"; //SCP
String i = new String("a") + new String("b"); //HEAP
String j = i.intern(); //SCP
System.out.println(c == i);
System.out.println(i == j);
System.out.println(c == j);
}

String i = new String("a") + new String("b");语句会在字符串常量池中生成两个字符串ab,在堆中生成3个对象:两个是由new String()生成的,另外一个是i。结合实例1的说明,可知:cj指向字符串常量池中指向地址,而i指向堆中。因此输出结果为:

1
2
3
false
false
true

实例3

1
2
3
4
5
6
7
8
9
@Test
public void test3() {
String c = "ab"; //SCP
String i = new String("ab"); //HEAP
String j = i.intern(); //SCP
System.out.println(c == i);
System.out.println(i == j);
System.out.println(c == j);
}

实例2中类似,String i = new String("ab");语句中构造函数里的字符串ab会直接指向由String c = "ab";语句在字符串常量池中生成的字符串的地址,在堆中生成一个字符串对象i。所以输出结果和实例2一样:

1
2
3
false
false
true

实例4

1
2
3
4
5
6
7
8
9
10
@Test
public void test4() {
String c = "ab"; //SCP
String b = "b";
String i = "a" + b; //HEAP
String j = i.intern(); //SCP
System.out.println(c == i);
System.out.println(i == j);
System.out.println(c == j);
}

根据本文开头的第2点规则,可知String i = "a" + b;语句中生成的变量i是位于堆中的,而cj都指向字符串常量池。因此输出结果为:

1
2
3
false
false
true

实例5

1
2
3
4
5
6
7
8
9
@Test
public void test5() {
String b = "b";
String i = "a" + b; //HEAP
String j = i.intern(); //SCP -> HEAP
String c = "ab"; //SCP -> HEAP
System.out.println(i == j);
System.out.println(c == j);
}

实例4中不同的是,虽然i是位于堆中,但是在执行String j = i.intern()时,由于字符串常量池中不存在字符串ab,根据本文开头的第3点规则,此时并不会直接把字符串ab复制在字符串常量池中,而是在常量池中为字符串ab生成指向堆中对象i的引用,包括之后的语句String c = "ab";c指向的也是常量池中指向堆中对象i的引用,所有cij指向的实际是同一个地址。因此输出结果为:

1
2
true
true

实例6

1
2
3
4
5
6
7
8
@Test
public void test6() {
String i = new String("ab"); //HEAP
String j = i.intern(); //SCP
String c = "ab"; //SCP
System.out.println(i == j);
System.out.println(c == j);
}

如果不仔细思考,可能会认为输出结果应该和实例5一样,但实际的输出结果却是如下:

1
2
false
true

参考实例3,想清楚String i = new String("ab");是会先在字符串常量池生成字符串ab这一点后,就很容易知道和实例5的区别了。

实例7

1
2
3
4
5
6
7
8
9
@Test
public void test7() {
final String b = "b";
String i = "a" + b; //SCP
String j = i.intern(); //SCP
String c = "ab"; //SCP
System.out.println(i == j);
System.out.println(c == j);
}

实例5的区别在于对象b是用final修饰的,可以看做局部常量,字符串对象i是由两个字符串常量通过+直接拼接而成,i将指向字符串常量池。因此输出结果为:

1
2
true
true

实例8

1
2
3
4
5
6
7
8
9
10
11
12
13
private void test8(final String b) {
String i = "a" + b; //HEAP
String j = i.intern(); //SCP -> HEAP
String c = "ab"; //SCP -> HEAP
System.out.println(i == j);
System.out.println(c == j);
}

@Test
public void test8() {
String b = "b";
test8(b);
}

这个实例的结果和实例7一样:

1
2
true
true

但是含义不同,虽然在方法test8(final String b)中,形参b是用final修饰的,但b的值仍然是外部传来的,所以不能看做字符串常量。因此i是执行堆中的对象,而jc是因为执行i.intern()之后,间接通过常量池指向了和i同一个地址。
调换一下上述方法中语句的位置,也可以验证改实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void test8_1(final String b) {
String c = "ab"; //SCP
String i = "a" + b; //HEAP
String j = i.intern(); //SCP
System.out.println(i == j);
System.out.println(c == j);
}

@Test
public void test8_1() {
String b = "b";
test8_1(b);
}

String c = "ab"; 语句提至方法内第一行后,在执行i.intern()时,由于常量池中已存在字符串ab,因此j将直接指向常量池中字符串ab的地址,而i是位于堆中的对象,所以输出结果为:

1
2
false
true

实例9

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test9() {
String b = "b";
String i = "a" + b; //HEAP_1
String l = "a" + b; //HEAP_2
String j = l.intern(); //SCP -> HEAP_2
String c = "ab"; //SCP -> HEAP_2
System.out.println(i.equals(j));
System.out.println(i == j);
System.out.println(l == j);
System.out.println(l == c);
}

结合前面的例子可知,ij是位于堆中两个独立的对象。由于有l.intern()操作,jcl最终都指向了同一个地址。因此输出结果为:

1
2
3
4
false
true
true
true

参考

  1. https://www.cnblogs.com/Kidezyq/p/8040338.html
  2. https://blog.csdn.net/soonfly/article/details/70147205
  3. https://www.geeksforgeeks.org/interning-of-string/