写这篇博客的目的起源于自己想要知道String提供的intern()方法的作用以及String内部的存储原理是怎样的?
如下代码:
String s1 = new String("cc");
s1.intern();
String s2 = new StringBuffer().append("cc").toString();
System.out.println(s2.intern() == s1);
运行环境是jdk1.8, 上面这段代码的执行结果为false,可是为什么是false呢?
一般我们在java当中创建String有两种方式:
1 直接使用双引号声明出来(这种也叫字面量):比如 String str = “aa”。
2 通过new String("aa")或者其他的比如StringBuffer或StringBuilder等。
在接下去之前,先来了解一下基本概念:
class文件里的常量池:一般我们在写java文件的时候,如果定义了"a"这种字符串,就会被编译器解析到class文件的常量池中,比如下面的代码
public void test() {
String s1 = "aa";
}
用javaap反编译一下,可以看到,编译器将字符串字面量解析到了常量池中。
方法里的字节码在运行时使用ldc加载常量池里的字符串对象ref放到栈顶,然后用astore_1命令将栈顶ref赋值给局部变量s1。
运行时常量池(Constant pool):我们知道每个class文件在jvm运行时会加载到虚拟机中,虚拟机中的每个class文件都会有一个自己的常量池,保存着class文件里的常量信息,比如class全限定名,父类的名字等。
String常量池(String Constant Pool):运行时常量池(Constant pool)里包含了String 常量池,底层实现是一个hashTable。也就是说我们的class文件里使用 " " 的声明的字符串字面量,最终都会放到String常量池。
在jdk1.6的时候,String常量池是放在PermGen区,就是我们熟悉的永久代,由于PermGen是固定的大小的,而且不易扩展,当存储的String对象太多,会引发OutOfMemoryException,到了jdk1.7 jvm团队将String常量池移动到了Heap中,并提供了通过命令行控制String常量池的大小。详情可以看https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931和https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6962930
放张图片方便理解:
String的intern方法作用:
如果字符串在String pool中不存在则创建一个,并返回该字符串的引用,如果在Heap有字符串对象,则返回将引用指向Heap中的对象,然后返回引用。
现在来解释一下代码:
String s1 = "Cat"; //编译期会在class文件常量池中,运行时会放到String常量池(String pool)。
String s2 = "Cat"//同上,运行时如果String pool中已经存在Cat了,就将Cat的引用返回给s2.
String s3 = new String("Cat"); //会在Heap中创建一个String对象Cat,如果Cat不存在于String pool中,也会在String pool中创建(注意不存在创建两个对象),因为上面已经创建了,所以这里只创建一次。
s1 == s2 //true 很好解释,都是同一个对象在String Pool
s1 == s3 //false 由于s3的String对象在Heap中,和s1不是一个对象,== 比较比的是对象的地址,所以不相等。
回答一下开始问题
String s1 = new String("cc"); //Heap创建了一个String对象,同时StringPool里创建了"cc"字符串
s1.intern(); //这里没有接收返回结果。只是把“cc”写入到StringPool,上一步StringPool里已经有"cc"了
String s2 = new StringBuffer().append("cc").toString(); // 这里只在Heap创建对象。String常量池有了"cc"
System.out.println(s2.intern() == s1); // 不可能相等,s1 是指向Heap里的String 对象,s2.intern()方法返回的是StringPool里“cc”的引用地址。
那么String的intern()方法,这个方法怎么用呢?
String s1 = "aa";
String s2 = new String("aa");
String s3 = s2.intern();
System.out.println(s3 == s1); // 执行的结果为true
这里intern方法的作用是返回String pool中 “aa”这个字符串字面量的引用(即使s2是通过new String在堆中生成对象,但是aa本质还是在String pool中),我们用s3保存这个引用。对比上面的图片,应该能理解执行的结果。s3和s1他们指向的就是String pool中"aa", 而s2指向堆中的String对象。
ok,再来看这段代码 s1 == s2 为什么会返回true呢?
String s1 = new String("1") + new String("1"); // 堆中创建"11"字符串对象,将引用给s1,同时String pool创建"1"这个字符串对象
s1.intern(); 由于Heap中已经存在一个, String pool 里的"11" 直接指向Heap中的对象了。
String s2 = "11"; // 返回String pool指向的那个对象。
System.out.println(s1 == s2); // 都是指向Heap中的对象,所以相等
总结:对于intern()这个方法,分为两种情况:
1 如果在调用intern()这个方法之前,String pool 里已经有对应的字符串了(一般就是我们直接写在java文件里的“cc”这样的字符串,在加载类的时候,虚拟机会帮我们把字面量生成String对象,存储在String pool 里),那么intern()直接返回String pool 里的String对象。
2 如果在调用intern()这个方法之前,String pool里没有对应字符串,而我们又是通过new String这种方式创建的(例如上面的new String("1") + new String("1"))。那么intern()会把"11"这个String对象(在堆上),放到String pool 里。后面的代码如果有 String s2 = "11";这种,那么s2就指向"11"这个堆上的String对象了。