最近在阅读《深入理解Jav虚拟机》的运行时常量池章节,看到“java语言并不要求常量池一定只有编译器才能产生…运行期间也可以将新的常量放入常量池,这种特性被开发人员利用得比较多的时String类的intern()方法。”于是我便去深入了解了一下。

1     public static void main(String[] args) {
2         String a="王者";
3         String b="荣耀";
4         String c=a+b;
5     }

上述代码中,我通过javap命令查看编译期的常量池,发现只有字符串”王者”和”荣耀”,并无”王者荣耀“,通过反编译得到的String c=a+b变成了,原来+号会被转为StringBuilder,并且拼接,注意当String c=”王者”+”荣耀”时,编译器则会优化,常量池里只有王者荣耀,并无“王者”和“荣耀”

1 String c= (new StringBuilder()).append(a).append(b).toString()

这就很有意思了,也就是说编译时常量池中并无”王者荣耀”了,因为编译期不会运算,只有在运行期才能执行方法得到结果。

由上图可知,”王者荣耀”直接指向堆,并且运行时常量池无”王者荣耀”

 

1     public static void main(String[] args) {
2         String a="王者";
3         String b="荣耀";
4         String c=a+b;
5         String d=c.intern();
6         System.out.println(d==c);
7     }         

上面为什么是true呢,接下来讲述inter()

JDK1.7后,intern方法先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,当字符串常量池中找不到对应的字符串时,而只是生成一个对该字符串的引用在字符串常量池。所以”王者荣耀”本身不会在字符串常量池,而常量池里面保存了在堆中王者荣耀的索引,所以为true。JDK1.7之前这里就不说了。

前面说了intern可以在程序运行时将新的常量放入运行时常量池,接下来就开始演示intern的强大用处

 public static void main(String[]  args) throws Exception {
        long start=System.currentTimeMillis();   //获取开始时间
        String[] arr = new String[10000000];
        Integer[] a= new Integer[10000000];
        for (int i = 0; i < 10000000; i++) {
            a[i] = (int)(1+Math.random()*(3));
        }
        for (int i = 0; i < 10000000; i++) {
            arr[i] = new String(String.valueOf(a[i]));
        }
        long end=System.currentTimeMillis(); //获取结束时间
        System.out.println("程序运行时间: "+(end-start)+"ms");
    }
    //程序运行时间: 14461ms
    public static void main(String[]  args) throws Exception {
        long start=System.currentTimeMillis();   //获取开始时间
        String[] arr = new String[10000000];
        Integer[] a= new Integer[10000000];
        for (int i = 0; i < 10000000; i++) {
            a[i] = (int)(1+Math.random()*(3));
        }
        for (int i = 0; i < 10000000; i++) {
            arr[i] = new String(String.valueOf(a[i])).intern();
        }
        long end=System.currentTimeMillis(); //获取结束时间
        System.out.println("程序运行时间: "+(end-start)+"ms");
    }
    //程序运行时间: 1688ms

很简单的小程序,就是有无调用intern方法,但程序运行时间差了10倍!

我们明确的知道,会有很多重复的相同的字符串产生,但是这些字符串的值都是只有在运行期才能确定的。所以,我们通过intern显示的将其加入常量池,这样可以减少很多字符串的重复创建。

new String(“王者荣耀”)和new String(“王者”)+new String(“荣耀”)不管是在编译期就确定放入常量池还是在运行期,都还是要在堆里面创建对象的。不同就是new String(“王者荣耀”)还要指向常量池。new String(“王者”)+new String(“荣耀”)不需要指向常量池

new String.intern中。这个过程是不会在Java堆中再创建一个String对象的。

注意,是因为随机数是运行期才知道的,intern把最先出现的,”1“,“2”,”3“,对象索引放入了常量池,下次在看见”1“,“2”,”3“,返回第一次在堆中String的对象地址,不用创建新对象,所以intern高效,使用intern()在堆中只会创建3个对象,而不使用intern则创建100000000个对象,并且常量池里并没有”1“,“2”,”3“

小问题,arri[]=new String(“王者荣耀”).intern()没什么意义的,虽然这样写比arri[]=new String(“王者荣耀”)效率高,可用代码验证,arri[]=new String(“王者荣耀”).intern()编译时常量池就出现“王者荣耀”,因为intern不会产生对象所以快,这里是直接引用了常量池中的字符串“王者荣耀”。但intern()并不是这样的初衷,而是为了解决运行时才出现的常量,不是解决编译时在字符串常量池的问题,这里比较抽象,多理解一下

笔者技术有限,如有错误,请指正,谢谢