Java中字符串的存储方式
Java中的字符串只能位于内存中的两个区域:常量池和Java堆。常量池维护了一个StringTable,它是一个hashtable,以字符串hashcode作为键,字符串引用作为值;Java堆中存储的就是普通的字符串对象。那么如何判断字符串到底位于哪一个区域呢?有以下几种情形。
情形一
String s = "123";
虚拟机在处理字面量"123"时,首先在本地栈中创建字符串"123",随后对其调用本地方法intern()。intern方法的作用是检查StringTable中是否存在这个字符串,如果有则返回对其的引用;如果没有,则将该字符串存入常量池中,并将其引用添加至StringTable中。 这里值得注意的是,如果该字符串已经存储于Java堆中,那么intern方法将不会再将其拷贝至常量池中,而是直接将其引用添加至StringTable中。
情形二
String s=new String("123");
与上面的直接定义不同,该语句会执行2个操作
- 将字面量"123"放入常量池中,将其引用添加至StringTable中(即情形一的操作)
- 在Java堆中创建一个字符串对象,里面的值与常量池中的"123"一致,返回引用
- 所以,该语句会创建2个对象,一个在堆中一个在常量池中,而引用指向的是堆中的对象
情形三
String s=new String("1")+new String("23");
这种情况底层等价于以下代码
StringBuilder stringBuilder=new StringBuilder();
stringBuilder.append("1").append("23");
String s=stringBuilder.toString();
这种情况与情形二最大的区别就是它不会在常量池中创建"123"对象,而只是创建一个堆中对象,将引用返回
介绍完了理论,接下来我们看具体场景
思考一下,下面的场景分别会输出什么结果?
场景一
String s1 = "123";
String s2 = s1.intern();
System.out.println(s1 == s2);
String s1 = new String("123");
String s2 = s1.intern();
System.out.println(s1 == s2);
答案分别是:true,false
- intern()方法在上面具体介绍过,主要是去StringTable中寻找是否存在调用字符串的引用(即确认该字符串是否已在常量池中),如果有则返回,没有则将当前字符串存入常量池中。
- 当执行s1.intern()时,会去StringTable中寻找是否存在"123"的引用,而无论是情形一还是情形二,常量池中都会存在"123",因此上面的s2和下面的s2存放的都是常量池中"123"的引用。但根据情形一,上面的s1存储的是常量池中"123"的引用;而根据情形二,下面的s1存储的是堆中字符串对象的引用。因此,返回的分别是true,false。
场景二
String h = new String("hw");
String h2 = h.intern();
String h1 = "hw";
System.out.println(h == h1);
System.out.println(h2 == h1);
System.out.println(h2 == h);
String s = new String("1") + new String("1");
String s2 = s.intern();
String s1 = "11";
System.out.println(s == s1);
System.out.println(s2 == s1);
System.out.println(s2 == s);
答案分别是false,true,false 和 true,true,true
- 上面的情况
- h存放的是堆中字符串对象的引用(情形二)
- h2存放的是常量池中字符串对象的引用
- h1存放的也是常量池中字符串对象的引用
- 下面的情况
- 根据情形三,s存放的是堆中字符串对象的引用,但此时常量池中并没有"11"的字符串对象
- intern()方法发现常量池中不存在对象,所以将调用字符串的引用(s2)存入StringTable中
- 因此s,s1,s2得到的都是同一个引用
场景三
String s = new String("1") + new String("1");
String s2 = s.intern();
String s1 = "11";
System.out.println(s == s1);
System.out.println(s2 == s1);
System.out.println(s2 == s);
String s = new String("1") + new String("1");
String s1 = "11";
String s2 = s.intern();
System.out.println(s == s1);
System.out.println(s2 == s1);
System.out.println(s2 == s);
答案分别是true,true,true 和 false,true,false
- 上面的情况:和场景二中的一样
- 下面的情况
- 根据情形三,s存放的是堆中字符串对象的引用,但此时常量池中并没有"11"的字符串对象
- 定义s1时因为常量池中没有对象,所以在常量池中创建"11"对象,往StringTable中添加引用并返回
- 因此根据intern()方法的特性,s2拿到的是s1的引用
参考的文章:
- https://juejin.im/post/5c25983cf265da61715e66cb
- https://www.zhihu.com/question/267818864/answer/329226635中海纳的回答