0%

面试常见的:String是否相等 问题原理探究

比较两个String对象是否相等的问题在笔试中常会碰到,但是一直没有理清它们究竟为什么相等,为什么不相等,今天终于把这个整理了一遍。首先,本文基于以下博主的参考博文总结,非常感谢他们。

https://www.zhihu.com/question/55994121

https://mp.weixin.qq.com/s/nWswsWKiSDEv4734VVUKkg

https://juejin.im/post/5b3197c4e51d4558b70cae46

三个常量池

  • Class文件中的常量池:存放字面量和符号引用,(用双引号引起来的字符串字面量都会进这里面)

  • 运行时常量池:存放在方法区中。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池(Constant Pool Table),存放编译期生成的各种字面量和符号引用,常量池这部分的内容将在类加载后进入方法区的运行时常量池

  • 全局字符串常量池:存放在中。HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容。

    因为是HashSet,所以不会保存相同的引用,所以字符串加载时会先判断字符串常量池中是否存储对堆中该字符串的引用,如果没有才在堆中创建字符串对象,并将其引用放入字符串常量池中;如果有则不加入

举例说明:

1
String s1="Java";//“Java”就是字符串字面量
  • 编译期:将双引号里的字符串字面量“Java”(无论是否在new String(“Java”)里)和符号引用s1加载到Class文件的常量池中

  • 类加载期:字面量和符号引用 加载到 运行时常量池中,但是字面量“Java”不一定会进入字符串常量池中,需要判断,如果存在则不进入,那这里“Java”不存在在字符串常量池中,所以就创建、保存引用、返回

例1

1
2
3
String s1="Java";//语句1
String s2=new String("Java");//语句2
System.out.println(s1==s2);//false:因为s1指向的是字符串常量中的引用,s2是堆内存中对象的引用

语句1和语句2都是创建一个字符串对象,但是有区别:

  • 语句1:s1保存的就是字符串常量池对“Java”字符串对象的引用

  • 语句2:new String(“Java”)先在字符串常量池中查找有没有该“Java”的引用,若没有则在堆内存中创建“Java”,而后通过new在堆内存中创建new String()对象,把“Java”拷贝赋值。那这里常量池中已经有了”Java”,就不用创建“Java”字符串对象了,只需创建new String()对象即可

    所以此时创建堆内存一个对象,new String()和”Java“堆内存的地址值不一样

    如果问String s = new String("Java");创建了几个对象?:1或2个对象,看字符串常量池是否有对堆内存的“Java”字符串对象的引用

例2

1
2
3
String s1=new String("Java");//语句1:在堆中创建了new String("Java") 字符串对象
String s2=new String("Java");//语句2:在堆中创建了new String("Java") 字符串对象
System.out.println(s1==s2);//false

原因:

语句1与语句2 创建的字符串对象在堆内存的地址不一样,所以s1、s2不是指的一块堆内存地址,所以为false

例3

1
2
3
4
5
   String s2 = new String("Java"); //语句1
s2.intern();//语句2,注意没有接收intern的返回值
String s1 = "Java"; //语句3

System.out.println(s1 == s2);//false:s1是字符串常量池的对堆内存中字符串字面量的引用

先介绍什么是intern方法

  • 如果字符串常量池中存在当前字符串的引用,那么直接返回字符串常量池中它的引用
  • 如果字符串常量池中没有此字符串的引用,说明堆中还没有该对象,就会先去堆中创建该对象,并将此字符串引用保存到字符串常量池中后, 再直接返回该字符串的引用

那对于语句2:intern() 发现字符串常量池中有“Java”字符串对象,返回该引用即可,注意没有接收返回的引用

s1指向的是堆内存new String()对象的地址,而s2指向的是字符串常量池对堆内存的“Java”字符串对象的引用,所以s1==s2返回false

例4

1
2
3
4
5
6
String s1 = "Java"; //语句1
String s2 = new String("Java"); //语句2
String s3 = new String("Java").intern();//语句3

System.out.println(s1 == s3);//返回true //因为s3是字符串常量池的对堆内存中字符串字面量的引用
System.out.println(s2 == s3);//返回false

“+”加号用法

例5

+号右边都是字符串字面量的,编译器在编译期就会将字符串字面量拼接在一起,所以s1存储的是字符串常量池中对堆内存中的“abc”字符串对象的引用

1
2
3
String s1 = "ab" + "c";
String s2 = "abc";
System.out.println(s1 == s2);//true

例6

+号右边 不全是字符串字面量的,至少有一个是对象的,那么+号是使用底层的StringBuilder对象,一路append,最后调用StringBuilder对象的toString方法得到一个String对象,并把它赋值给符号引用

  • 注意:这个toString方法会new一个String对象
1
2
3
4
   String s1 = new String("Ja")+new String("va");//语句1
s1.intern();//语句2,注意没有接受intern的返回值
String s2 = "Java";//语句3
System.out.println(s1 == s2); //返回true

语句1:

  • 字符串常量池中没有“Ja”、“va”字符串字面量的堆引用,先创建这两个字面量,然后将引用保存到字符串常量池中;

  • 然后 +号:创建一个new String(“Java”),注意:没有把Java的引用放入字符串常量池

  • 此时堆中一共有三个对象:new String(“Ja”)、new String(“va”)、new String(“Java”)

语句2:

  • intern会去查找字符串常量池中是否有“Java”这个堆引用,发现没有,就将new String(“Java”)字符对象的引用保存到字符串常量池中,并返回当前字符串的引用(但没有接收)

语句3:

  • 发现字符串常量池已经存在引用了,直接返回(拿到的也是与s1相同指向的引用)

结果:因为s1、s2指向的都是new String(“Java”)这个对象,所以返回true