Java中的String与intern方法

来源:互联网 发布:java字符串转json数组 编辑:程序博客网 时间:2024/06/02 00:21

常量池

在理解Java中的String之前有一个必须要知道的概念-常量池
在java的class文件中,有一块常量集中存放的区域,这块地方被称为常量池。常量池中存储的常量通常包括关于类,方法,接口等中的常量,以及字符串常量,如String s = “java”这种申明方式;当然也可扩充,执行器产生的常量也会放入常量池。而且在JDK1.7对常量池所处的位置也做了变动。在1.7以前,常量池位于JVM运行时内存的方法区(永久代或叫PermGen)。而到了1.7,常量池逐渐迁移到了MetSpace(元空间)中,此空间不存在于JVM中,其使用本地内存。

常量池和堆中的字符串

存储在常量池中的字符串通常包括以下形式
1. String a=”a”这种字符串字面值类的声明,这种声明会先去常量池中寻找是否有字符串”a”(或指向字符串的引用),如果有,则直接返回此引用。如果木有,则将字符串放入常量池中,并返回其引用;
2. String ab=”a”+”b”这种字符串字面值类的拼接,编译完成后,会直接返回一个”ab”字符串,处理过程和a类似。
3. String ab=new String(“ab”)这种形式声明的字符串是直接在堆中生成一个字符串对象。
4. String ab=a+”b”这种形式的字符串拼接过程要分两种情况讨论。假如a变量是个普通变量,这种情况下,通过生成一个StringBuilder,并调用append方法。这种方式生成字符串和使用new方式生成的字符串并没有区别。都是直接在堆上生成字符串。而假如a是final类型的,在编译时,编译期会自动吧a变量替换成”a”字符串,从而该表达式类似于第二种情况,直接在常量池中生成字符串。
以一段代码结果来验证以上陈述

public class StringTest {    public static void main(String[] args) {        String b="b";        final String finalb="b";        String ab="ab";        String abNewString=new String("ab");        String abString="a"+"b";        String abVar="a"+b;        String abFinalVar="a"+finalb;        System.out.println(abNewString==ab);        System.out.println(abString==ab);        System.out.println(abVar==ab);        System.out.println(abFinalVar==ab);    }}

上面程序的执行结果如下:

falsetruefalsetrue

下面再来看下其反编译后的结果:

public class com.person.blogcases.StringTest {  public com.person.blogcases.StringTest();    Code:       0: aload_0       1: invokespecial #1                  // Method java/lang/Object."<init>":()V       4: return  public static void main(java.lang.String[]);    Code:       0: ldc           #2                  // String b       2: astore_1       3: ldc           #2                  // String b       5: astore_2       6: ldc           #3                  // String ab       8: astore_3       9: new           #4                  // class java/lang/String      12: dup      13: ldc           #3                  // String ab      15: invokespecial #5                  // Method java/lang/String."<init>":(Ljava/lang/String;)V      18: astore        4      20: ldc           #3                  // String ab      22: astore        5      24: new           #6                  // class java/lang/StringBuilder      27: dup      28: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V      31: ldc           #8                  // String a      33: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;      36: aload_1      37: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;      40: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;      43: astore        6      45: ldc           #3                  // String ab      47: astore        7      49: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;      52: aload         4      54: aload_3      55: if_acmpne     62      58: iconst_1      59: goto          63      62: iconst_0      63: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V      66: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;      69: aload         5      71: aload_3      72: if_acmpne     79      75: iconst_1      76: goto          80      79: iconst_0      80: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V      83: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;      86: aload         6      88: aload_3      89: if_acmpne     96      92: iconst_1      93: goto          97      96: iconst_0      97: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V     100: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;     103: aload         7     105: aload_3     106: if_acmpne     113     109: iconst_1     110: goto          114     113: iconst_0     114: invokevirtual #12                 // Method java/io/PrintStream.println:(Z)V     117: return}

在0、3、6行分别从常量池中将字符串引用( “b”,”b”,”ab” )压入栈,而2、5、8行则分别将入栈的String引用赋于本地变量1、2、3(b,finalb,ab)中,从第9行到第18行是整个String abNewString=new String("ab"); 的反编译结果。而20、22两行则是String abString="a"+"b"; 反编译的结果,可以看到,它也是直接从常量池中获取字符串引用的。从24到43行是String abVar="a"+b; 反编译的结果,这是Java的一个语法糖,类似这种字符串变量相加的情况,Java会生成一个StringBuilder并调用其append进行拼接,最后调用toString来返回结果。而45、47两行则是String abFinalVar="a"+finalb; 反编译的结果,可以看到其也是从常量池中直接获取到的字符串常量。这样的话上面程序的执行结果就比较好理解了,因为ab、abfinalVar、abString三个变量都是从常量池中获取的字符串变量的引用,因此在使用==判断的时候会是true。而对于abNewString、abVar,它们都是在堆上创建出来的对象,因此引用是不会和ab相等的,并且abNewString和abVar也不会是同一个引用。
理解了这些,其实intern就很好理解了,对一个变量调用intern方法的过程就相当用字符串字面值声明一个字符串变量的过程。
例如

String a=从文件或者哪里读来的字符串;String b=a.intern()

a.intern方法会先拿a字符串去常量池中寻找,如果找到了和其相同的字符串,则直接返回其引用。如果木有找到则会将a字符串的话要分两种情况。在JDK1.7以前,JVM会复制一份a字符串到常量池中,然后返回常量池中字符串的引用。而1.7以后,JVM不会将字符串往常量池中复制一份了,而是直接在常量池中存储一个指向a变量的引用,然后返回常量池中的这个引用。由这个特点可以看出来,对于程序中有大量重复字符串的场景,使用intern方法可以一定程度上节省内存消耗。

使用场景

由于常量池所在方法区,因此一般容量相较于堆空间较小。其适用场景也就比较固定。一般适用于存储大量字符串的集合类。例如要存储一亿个字符串,其中不同的字符串就3类,这时使用intern方法就可以显著的减少对堆空间浪费,因为所有相同字符串的引用都指向同一个字符串对象。不适合大量非重复字符串的场景,例如这一亿个字符串各不相同。这时如果都使用intern,在JDK1.7以前会出问题,因为对于调用intern后,jvm会复制字符串至常量池,常量池大小不够的话,就OOM了。JDK1.7以后,不再复制字符串实例置常量池后,这种场景先的适用性有所缓和。
举个面试的时候经常会被问到的问题作为例子,有一个日志文件,里面记录了各个Ip的访问信息,找出前十个访问记录最多的Ip,假设我们要先把这些Ip访问信息读到内存里(当然肯定有更好的方法)。这个时候使用intern方法就很合适,因为来访问的Ip必定有大量重复的。