首先因为jdk1.7对该方法进行了改动,所以我们只分析1.7的现象。
在分析这个方法前,我们先来看看HotSpot的字符串常量池
他的存在,是为了减小对象的创建,我们不要把它想的过于复杂,比如我们在程序中写了一个String a = "abc";
那么这个abc就会存储在字符串常量池中,下次再遇到这个,就不会再创建一个abc了,而是直接去字符串常量池中去找,所以String a = "abc";
和String b = "abc";
这两个变量,a==b
是true
的。因为他们都指向同一个字符串常量池。(字符串常量池1.7之前在方法区,1.7之后被挪到了堆,方法区也被挪到了元空间)。
简单了解了一下字符串常量池后,我们再来看一个经典的面试题
String s1 = new String(“abc”);这句话创建了几个字符串对象?
答案是 1个或者2个,为啥呢,如果字符串常量池中已存在字符串常量(或者引用,后面再说)“abc”,则只会在堆空间创建一个字符串常量“abc”。如果池中没有字符串常量“abc”,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。
比如下面这个示例
1 |
|
这个如果比较好懂,我们就再接着看一个稍微复杂一点的
1 |
|
我们来解读一下这个test,第一行str1和第二行str2 毫无疑问,在字符串常量池中产生,第三行和第四行有什么区别呢,第三行 两个字符串常量相加,依然在字符串常量池,第四行了两个变量相加,那么就会在堆中产生了。这里我们做一个说明:
String str3 = “str” + “ing”; 在编译期间,这种拼接会被优化,编译器直接帮你拼好,所以str3相当于直接赋值为“string”
而相加的过程中一旦出现了对象,就不会做优化,因为这是一个对象,内存不是确定的,没有写死,无法实现优化。而且在相加的过程中,java会先new出一个StringBuilder,然后调用append()方法来将+号两遍的字符串拼接起来,然后toString()之后返回给=号左边的变量,也就是说,最后得到的是一个new出来的字符串
这样一来,结果就很好理解了。
我们再看一个例子
1 |
|
我们分析一下,ab是堆,“ab”是常量池,所以false很合理,如果我们吧a变成final的,那么结果就是true了。
1 |
|
这就是我们刚才所说的优化了,因为a是final不可变的,所以编译的时候,a+”b”=”a”+”b”=”ab”;
为啥说intern要先举一下上面的例子呢,因为String的intern方法就是针对这个来设计的优化。
intern是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
jdk1.7后,如果对象只存在堆中,会拷贝对象的引用到常量池中
了解了基本定义之后,我们再来看下面这个例子
1 |
|
第一行会在字符串常量池和堆中分别创建两个对象,s1指向堆中的引用,没有问题,s1.intern()指向常量池,此时常量池中已经有了“计算机”,所以s2此时就等于常量池中的引用,s3直接指向常量池。
看了这个简单的例子后,我们稍微看个复杂点的
1 |
|
这个时候我们可能会有疑问str1 == “abcdef” 怎么可能为true呢,一个是堆,一个是常量池。我们一步步看,第一句话,我们分析一下,创建了几个对象。
答案是5个,常量池中有两个,分别是abc和def,堆中有三个,abc,def和abcdef,这个没啥疑问吧,然后第二行,调用了str1.intern()。这个时候发生了什么,它去常量池中找abcdef,发现没有,做了个什么操作呢,把堆中的引用存到了常量池中,(1.7之前是拷贝值,1.7之后是拷贝引用,因为都在堆里面了,为了优化,没必要存两份了)。这个时候,常量池中存在了abcdef,这个abcdef就是str1中堆的值。所以 这个时候我们再来比较str1和abcdef,其实都是指的堆。我们把这个例子加一句话
1 |
|
我们只在第一行加一句话,其他的都不动。结果却是翻天覆地的。如果你对intern有了一个差不多的了解后,我们再来分析一下,第一句话,在常量池中创建了一个abcdef,第二句话效果不变,我们再来看第三句话,此时str1.intern()去常量池中找,发现有了一个常量了,这个时候 它就指向了常量池中的abcdef了,所以结果不难理解了。
了解了这个之后,我们再来看一个有趣的例子
1 |
|
根据我们刚才的推测,这俩应该都是true,因为都是指向的堆中的引用。但是实际上,结果是:
1 | true |
为啥第二个是false呢,这是因为“java”是个关键字, 这个字符串在执行StringBuilder.toString()之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求“首次遇到”的原则,“计算机软件”这个字符串则是首次出现的,因此结果返回true。
最后,我们再看一个例子
1 |
|
这个例子再次给我们证明了,如果常量池中不存在,存储对象的引用,所以第一个是true,第二个ab2.intern()的时候,发现已经存在了ab1的引用,所以指向了ab1,这两行,谁在前面,谁是true。