String.intern()

创建String的几种方式以及背后的存取规则:

1,String str1 = new String("1900");(后面不带+),这个过程中,实际上有两个对象生成,一是在堆上创建了"1900"这个字符串对象,同时,检查常量池,池中如果有"1900",不管是指向"1900"的引用还是实打实的串,就不创建新的"1900",如果没有,则创建"1900"放入常量池,暂且不讨论常量池因为JDK版本的不同而导致的位置不同。 

2,String str2 = "1900";此时直接将"1900"加入常量池,同样,检查有没有,有则返回引用没有则加入。

3,常量字符串的"+"拼接操作,如String str3 = "19"+"00";同样,检查常量池,从而决定创建还是引用。

4,final修饰的串,final修饰的串在编译器就会替换成常量,即等号后面是啥,就将它送去常量池。当然,同上面一样,也需要检查常量池。

5,String str4 = str3 + "hello";这种变量+常量的形式,则会调用StringBuilder在堆上创建对象。注意是堆。

关于JDK版本导致的intern()方法的不同:

JDK1.6及以前。intern()检查常量池如果发现未存在放入常量池的是字符串。

JDK1.7及以后。intern()检查常量池如果发现未存在放入常量池的是其在堆上的对象的引用,或者换个说法,存的是堆上对象的一个一模一样的副本。

几个常见的问题及其解释:(以JDK1.7+为标准 )

我在蛮多博客都看到了这个问题的描述,可是看得我晕头转向,不知所措......

问题1:

String str1 = new String("1900");
        String intern = str1.intern();
        System.out.println(intern == str1);

第三行输出什么?答案是false。

解释:第一行在堆中创建了"1900",同时在常量池也有"1900",第二行调用intern,查看常量池,发现常量池有"1900",所以返回常量池这个对象的引用,而第三行比较的即是在堆上的该对象的引用和在常量池中该对象的引用,自然是不等。

问题二:

String str1 = new String("19") + new String("00");
        String str2 = "1900";
        str1.intern();
        System.out.println(str2 == str1);

第三行输出什么呢?答案还是false

解释:刚开始我也想了挺久为啥是false,原因是我的阅读理解有问题或者说粗心

首先是第一行,可以分为两步来看,第一步是在堆上创建了两个对象,一个是"19",一个是"00",同时跟之前一样,常量池中创建了"19"和"00"两个对象,第二步,也就是连接的步骤,是指在对上创建了“1900”,而常量池并没有"1900"。

第二行用字面值赋值创建str2,就会先检查常量池,发现并没有“1900”,所以它在常量池创建了“1900”。

第三行也是当时最误导我的地方,str1.intern(),先检查常量池,此时常量池有“1900”,所以返回了“1900”的引用,但是这里是返回值,不是将str2的地址改变,而此处没有变量来接住返回值,所以这一行并没有什么卵用。

故第四行一个是堆上的str1,一个是常量池的str2,自然不等。

问题三:改为由变量接住

 String str1 = new String("19") + new String("00");
        String str2 = "1900";
        String str3 = str1.intern();
        System.out.println(str2 == str3);

显然这里会返回true,解释就不多解释了。

问题四:

new String("1900")和new String("19")+new String("00")有啥不同?

区别在于,其实前面也说过,new String("1900")之后,在堆里,常量池都会有"1900",但是如果是后者,那么在常量池中是没有"1900"的。

问题五:

        String str1 = new String("19") + new String("00");
        str1.intern();
        String str2 = "1900";
        System.out.println(str2 == str1);

        String str3 = new String("1900");
        str3.intern();
        String str4 = "1900";
        System.out.println(str3 == str4);

答案:

true
false

有了以上的铺垫,这里就好分析了,首先前四行,第一行运行过后,堆中有"19","00","1900",常量池中有"19","00",经过第二行,由于JDK1.7+ intern的特性,在常量池中生成了一个str1的副本,到第三行,检查常量池,发现有"1900"(调用equals()),于是返回该引用,而这个引用就是sr1 ,所以两者相等。

后面四行,堆上创建了另外一个值也为"1900"的str3,而这时候调用intern,本来常量池中有"1900",所以这一行没起作用,str4则是引用了常量池中的"1900",显然Str3和 str4不等。

问题六:

关于final,以及"+"

String s1 = "abc";
        String s2 = "a";
        String s3 = "bc";
        String s4 = s2 + s3;
        System.out.println(s1 == s4);
        String s5 = "abc";
        final String final_str1 = "a";
        final String final_str2 = "bc";
        String s6 = final_str1 + final_str2;
        System.out.println(s5 == s6);

答案:

false
true

分析:

前五行:前三行都是直接在常量池创建字符串,第三行拼接实际是调用StringBuilder,建立在堆上,所以s1和s4不等。

后五行:final修饰的会在编译器被替换为常量,所以s6相当于两个常量字符串相加(s6="a"+"bc"),根据前面的规则,直接在常量池中加入,而在这之前常量池中有abc,所以自然是相等的。

总之,如果是连接变量和常量,则JVM是无法优化的,则创建在堆上,除非用final修饰。

总结:

以上的机制都是JVM为了节约内存所做的优化,实际上细心的会发现,各大包装类中都有类似机制。比如Integer类里面:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

在-128到128之间的数是直接返回的,而其他则是创建新对象。

最后,感谢两位大佬的博客,让小弟受益匪浅:

https://blog.csdn.net/qq_34115899/article/details/86583262

https://blog.csdn.net/seu_calvin/article/details/52291082

原文地址:https://www.cnblogs.com/Yintianhao/p/12273714.html