本文主要解释java的inten方法的作用和原理,同时会解释一下经常问的String面试题。
首先先说一下结论,后面会实际操作,验证一下结论。intern方法在不同的Java版本中的实现是不一样的。Java6之前是一种实现,Java6之后也就是Java7和Java8是另外一种实现。
先说一下intern方法的定义 在Java的String类中是这样定义的,是一个本地方法,其中源码由C实现
public native String intern();
再来看一下源码的注释描述:
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
翻译过来的意思就是:如果常量池中已经有了此字符串,那么将常量池中该字符串的引用返回,如果没有,那么将该字符串对象添加到常量池中,并且将引用返回。
首先要明白,这里注释的该字符串是调用此方法的字符串,返回的是引用。
下面介绍一个new String的知识点,下文再讨论intern的实现的会用到。
有个经典的Java基础面试题,new String("xyz")会创建几个对象?
动手实践后,发现再new String("xyz")有可能会创建一个(不好验证,但是可以通过下文的分析得出结论),也有可能会创建两个(可验证)。
结论:如果常量池中没有 xyz,那么就会创建两个,现在堆中创建一个,然后将对象copy到常量池中,也就是第二次创建,堆中和常量池中是两个对象。
上图的版本是Java8版本。
Java6版本:
intern方法作用:确实如上述注释上所描述,如果常量池中没有字符串,则将该字符串对象加入常量池,并返回引用。
** 这里需要注意:Java6中常量池是在方法区中,而Java1.6版本hotspot采用永久带实现了方法区,永久代是和Java堆区分的,即就是常量池中没有字符串,那么将该字符串对象放入永久带的常量池中,并返回其引用。
Java7和Java8版本:
intern方法作用:和注释描述的并不同,
如果常量池有,那么返回该字符串的引用。
如果常量池没有,那么如果是”a“.intern调用,那么就会把”a“放入常量池,并返回”a“在常量池中的引用。
如果是new String("a").internal ,其中在 new String的时候上文已经说到过,会在堆和常量池各创建一个对象,那么这里返回的就是常量池的字符串a的引用。
如果是new StringBuilder("a").internal,其中new StringBuilder会在堆中创建一个对象,常量池没有,这里调用intern方法后,**会将堆中字串a的引用放到常量池,注意这里始终只是创建了一个对象,
返回的引用虽然是常量池的,但是常量池的引用是指向堆中字串a的引用的。
上述粗体字描述部分也就是不同之处,也是intern方法在Java6 之后的使用变化如何体现。
再简单总结一下Java7和Java8的intern方法作用:如果常量池没有,那么会将堆中的字符串的引用放到常量池,注意是引用,然后返回该引用。为什么Java7和Java8会不一样呢,原因就是 Java7之后(部分虚拟机,Hotspot,JRockit)已经将永久代的
常量池、静态变量移出,放入了Java堆中,而永久代也在Java8中完全废弃,方法区改名为元空间。既然常量池已经在Java6之后放入了堆中,那么如果堆中已经创建过此字符串的对象了,那么就没有必要在常量池中再创建一个一毛一样的对象了,直接将其引用拷贝
返回就好了,因为都是处于同一个区域Java堆中。
下面看几个实践:来验证一下上述的结论:
1、下图 在new的时候已经创建了两个对象,第二行,只是获取的第一行创建的常量池的对象的引用,实际的对象已经创建过了。这里是两个不同的对象,返回false。
2、和上述一样,只不过这一次第一行,现在常量池创建了对象,第二行发现常量池已经有了,只在堆上创建了一次对象,但仍然是两个对象,引用不同,返回false。
3、第一行,Strignbuilder只会在堆中创建一个对象,第二行调用intern方法后,会将堆中的引用放到到常量池中。第三行发现常量池中已经有这个字符串的引用了,直接返回。因此是同一个引用,返回的都是第一次创建的堆中字串的引用
4、和上述3的不同之处在于没有调用intern方法,因此结果输出不一样。
5、new String之后使用 + 在Java中会进行编译优化,编译成字节码指令后,会将+ 优化成 先new Stringbuilder对象,然后调用append方法进行拼接。
如下图:
反编译生成字节码:
因此这里s1最终创建的时候,xyzz字符串并没有在常量池创建,只是在堆中创建了,因为就如同上面的3一样,是new Stringbuilder操作。
所以在调用intern操作后,将其堆中的引用放入常量池并返回。所以后面的结果都是true,因为至始至终都是堆中的一个对象。
6、和上述5是相反的,结果输出也不同。
总结:
在Java6之后,使用intern可以起到优化的作用,但也要看具体的情况,比如在使用加号做字符拼接的时候,如果不想在因为其他的操作在常量池中重新创建相同的对象,那么调用intern方法,在常量池中只会放入一个引用,这时候只创建了一个对象。