01丨Java|Java--深入理解字符串的String#intern()方法奥妙之处

学习背景 进入正文学习字符串的intern()方法之前,先给下这4个问题,看下自己是否都知道答案?

1、String s1 = “a” + “b”; //创建了几个对象?
2、String s2 = new String(“ab”); //创建了几个对象?
3、String s3 = new String(“a”) + new String(“b”); //创建了几个对象?
4、String s4= new String(“a”) + new String(“a”); s4.intern(); //创建了几个对象?
如果都清楚,恭喜你,大佬一枚,不用往下学习了,哈哈哈!
那如果不太确定或者需要加深自己的理解,建议进入正文一起来了解下吧!
当然,也可以拉到最后有答案!
String#intern()示例代码 先来执行一下String调用intern()方法的一段示例代码:
public class StringInternTest {public static void main(String[] args) {String reference1 = new String("a"); reference1.intern(); String reference2 = "a"; System.out.println(reference1 == reference2); String reference3 = new String("a") + new String("a"); reference3.intern(); String reference4 = "aa"; System.out.println(reference3 == reference4); } }

JDK1.6 执行输出结果:
false false

JDK1.7 执行输出结果:
false true

大家可以先思考一下为什么结果是这样的?往下会具体介绍!
String##intern()源码 先来看一下intern()方法的JDK源码如下:
/** * Returns a canonical representation for the string object. * * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * * 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. * * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * The Java™ Language Specification. * * @returna string that has the same contents as this string, but is *guaranteed to be from a pool of unique strings. */ public native String intern();

很显然通过源码可以看到intern()是一个native本地方法,但是native具体实现源码已经被隐藏了,这是一个历史故事了,SUN公司在JDK7开发期间,由于技术竞争和商业竞争陷入泥潭,无力再投入精力继续研发JDK,Oracle半路杀出直接收购Sun公司,Oracle接管JDK的研发后,发版了自己的Oracle JDK,Oracle的native底层等很多源码就被隐藏了,不过Oracle官方也声明OpenJDK和Oracle JDK7及以后版本,源码几乎是一模一样的,想要了解native底层源码具体实现过程,可以下载开源的OpenJDK的源码进行查看。
OpenJDK官网:https://hg.openjdk.java.net/
GitHub也开源啦:https://github.com/openjdk/jdk
例如String对应的OpenJDK底层源码主入口:jdk7\jdk\src\share\native\java\lang\String.c
Java_java_lang_String_intern(JNIEnv *env, jobject this) {return JVM_InternString(env, this); }

native底层方法的实现,需要掌握C和C++的语法,学习门槛要求比较高,这里不是我们要学习的重点,不做具体介绍。
String#intern()方法作用 前面JDK源码intern()方法的英文注释已经说明了intern()方法的有具体用途了,网上也有很多说明,不过这里我以个人的理解以及话术简单概括下intern()方法的作用如下:
(1)只要调用String对象的intern(),都会去找到字符串常量池,然后判断String对象的字符串内容是否已经存在常量池中,不存在,则往字符串常量池中创建该字符串内容的对象(JDK6及之前)或创建新的引用并指向堆区已有对象地址(JDK7之后),存在则直接返回。
(2)JDK7时,字符串常量池从永久代脱离,迁移到堆区中,相比于JDK6,变化不只是字符串常量池迁移到堆区而已,另一个变化就是调用字符串对象的intern()方法,如果字符串常量池中不存在该字符串内容的对象,则不会再像JDK6直接往字符串常量池中创建该字符串内容的对象,而是创建一个新的引用并指向堆区已有对象地址,实现字符串常量池和堆区字符串共用的目的,效率更高。
JDK6 String#intern()执行说明 一张图介绍前面示例代码JDK6执行过程如下:
01丨Java|Java--深入理解字符串的String#intern()方法奥妙之处
文章图片

/** * JDK6 String#intern()执行说明 */ public class StringInternTest {public static void main(String[] args) {//Step6.1 //创建了2个对象,分别是堆区的String对象和字符串常量池中的"a"对象,reference1引用指向在堆区中的对象地址 String reference1 = new String("a"); //Step6.2 //判断字符串常量池,是否该字符串"a",此前,池中已经有该对象了,因此会返回池中的对象地址的引用 reference1.intern(); //Step6.3 //字符串常量池中已存在字符串"a",因此reference2引用直接指向对象在字符串常量池中的地址 String reference2 = "a"; //reference1指向对象地址是在堆区,reference2指向对象地址是在永久代的常量池,显然不可能一样 System.out.println(reference1 == reference2); //Step6.4 //创建了2个对象,分别是在堆区的String对象(内容是"aa")和字符串常量池中的"a"对象 //reference3引用指向对象在堆区中的地址,这过程还会在堆区创建了两个无引用的"a"对象,这里不做讨论 String reference3 = new String("a") + new String("a"); //Step6.5 //判断永久代中的字符串常量池,是否存在该字符串"aa",这里是首次出现,因此直接将字符串拷贝并放到池中 reference3.intern(); //Step6.6 //池中已存在该字符串,reference2引用直接指向对象在永久代字符串常量池中的地址 String reference4 = "aa"; //同样,reference3指向堆区地址,reference4指向永久代常量池中的地址,显然不可能一样 System.out.println(reference3 == reference4); } }

JDK7 String#intern()执行说明 一张图介绍前面示例代码JDK7执行过程如下:
01丨Java|Java--深入理解字符串的String#intern()方法奥妙之处
文章图片

/** * JDK1.7 String#intern()执行说明 **/ public class StringInternTest {public static void main(String[] args) {//Step7.1 //创建了2个对象,分别是堆区的String对象和字符串常量池中的"a"对象,reference1引用指向在堆区中的对象地址 String reference1 = new String("a"); //Step7.2 //判断字符串常量池,是否该字符串"a",此前,池中已经有该对象了,因此会返回池中的对象地址的引用 reference1.intern(); //Step7.3 //字符串常量池中已存在字符串"a",因此reference2引用直接指向对象在字符串常量池中的地址 String reference2 = "a"; //reference1指向对象地址是在堆区,reference2指向对象地址是在堆区的字符串常量池,引用指向的对象地址不一样 System.out.println( reference1 == reference2); //Step7.4 //创建了2个对象,分别是在堆区的String对象(内容是"aa")和字符串常量池中的"a"对象(注意并不会创建"aa"对象) //reference3引用指向对象在堆区中的地址,这过程还会在堆区创建了两个无引用的"a"对象,这里不做讨论 String reference3 = new String("a") + new String("a"); //Step7.5 //判断堆区的字符串常量池中,是否存在该字符串"aa",显然这里是首次出现 //但并不像JDK6会新建对象"aa"存储,而是存储指向堆区已有对象地址的一个新引用 reference3.intern(); //Step7.6 //指向池中已有该字符串的新引用,reference4引用直接指向字符串常量池中的这个新引用,新引用则指向堆区已有对象地址 String reference4 = "aa"; //reference4指向新引用,而新引用则指向堆区已有对象地址,跟reference3引用直接指向的对象地址是同一个 System.out.println(reference3 == reference4); }

经典面试问题之创建了几个对象? 在实际的Java面试当中,经常会被问到字符串创建了几个对象的问题,主要是考察学习者对于对象的实例化以及字符串常量池在JVM结构体系中是如何运行的,个人觉得比较常见问题,无法就是如下几个:
1、最简单的比如:String s1 = “a” + “b”; 创建了几个对象?
答:最多1个,多个字符串常量相加会被编译器优化为一个字符串常量即"ab",如果字符串常量池不存在,则创建该对象。
2、相对简单的比如:String s1 = new String(“ab”); 创建了几个对象?
答:1个或2个,使用new实例化对象,必然会在堆区创建一个对象,另外一个就是如果在字符串常量池中不存在"ab"这个对象,则会创建这个"ab"常量对象。
3、稍微难一点的比如:String s2 = new String(“a”) + new String(“b”); 创建了几个对象?
答:至少4个,最多6个
1个new StringBuilder()和2个new String()
另外"a"、"b"可能会在常量池新建对象
最后1个是,StringBuilder()的toString()方法底层实现是new String(value, 0, count)
有的同学可能会有疑问,那"ab"字符串不会在常量池中也创建吗?
答案是,不会,最后StringBuilder的toString() 的调用,并不会在字符串常量池中去创建"ab"对象。
两个new String相加会被优化为StringBuilder,可以通过javac和javap查看汇编指令如下:
javac InternTest.java
javap -c InternTest
public class com.justin.java.lang.InternTest {public com.justin.java.lang.InternTest(); Code: 0: aload_0 1: invokespecial #1// Method java/lang/Object."":()V 4: returnpublic static void main(java.lang.String[]); Code: 0: new#2// class java/lang/StringBuilder 3: dup 4: invokespecial #3// Method java/lang/StringBuilder."":()V 7: new#4// class java/lang/String 10: dup 11: ldc#5// String a 13: invokespecial #6// Method java/lang/String."":(Ljava/lang/String; )V 16: invokevirtual #7// Method java/lang/StringBuilder.append:(Ljava/lang/String; )Ljava/lang/StringBuilder; 19: new#4// class java/lang/String 22: dup 23: ldc#8// String b 25: invokespecial #6// Method java/lang/String."":(Ljava/lang/String; )V 28: invokevirtual #7// Method java/lang/StringBuilder.append:(Ljava/lang/String; )Ljava/lang/StringBuilder; 31: invokevirtual #9// Method java/lang/StringBuilder.toString:()Ljava/lang/String; 34: astore_1 35: return }

6、最难的无非就是再调用intern()方法,比如:
String s3= new String(“a”) + new String(“a”);
s3.intern(); 创建了几个对象?
【01丨Java|Java--深入理解字符串的String#intern()方法奥妙之处】答:最少4个,最多7个
1个new StringBuilder()和两个new String
另外"a"、"b"可能会在常量池新建对象
最后1个是,StringBuilder()的toString()方法底层实现是new String(value, 0, count)
最后是调用intern()方法,会去找到字符串常量池,判断"ab"是否存在,不存在,则创建"ab"对象。

    推荐阅读