String常量池详细分析

先举个例子

        String a = "hello";
        String b = new String("hello");
        System.out.println(a == b);//false

        String c = "world";
        System.out.println(c.intern() == c);//true
        
        String d = new String("mike");
        System.out.println(d.intern() == d);//false

        String e = new String("jo") + new String("hn");
        System.out.println(e.intern() == e);//true

        String f = new String("ja") + new String("va");
        System.out.println(f.intern() == f);//false

 字符串常量池

字符串常量池了,是java为了节省空间而设计的一个内存区域,java中所有的类共享一个字符串常量池。比如A类中需要一个“hello”的字符串常量,B类也需要同样的字符串常量,他们都是从字符串常量池中获取的字符串,并且获得得到的字符串常量的地址是一样的。

实际上,为了提高匹配速度,即更快的查找某个字符串是否存在于常量池,Java在设计字符串常量池的时候,还搞了一张stringtable,stringtable有点类似于我们的hashtable,里面保存了字符串的引用。我们可以根据字符串的hashcode找到对应entry,如果没冲突,它可能只是一个entry,如果有冲突,它可能是一个entry链表,然后Java再遍历entry链表,匹配引用对应的字符串,如果找得到字符串,返回引用,如果找不到字符串,会把字符串放到常量池中,并把引用保存到stringtable里。

String对象底层其实是一个字符数组和一个hash值,从下面String源码可以看出。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];

    /**
     * Initializes a newly created {@code String} object so that it represents
     * an empty character sequence.  Note that use of this constructor is
     * unnecessary since Strings are immutable.
     */
    public String() {
        this.value = "".value;
    }

    /**
     * Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument; in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.
     *
     * @param  original
     *         A {@code String}
     */
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

    //......
}

怎么样才能进入字符串常量池

往细讲,只有执行了ldc指令的字符串才会进入字符串常量池。什么意思呢?先看一个例子:

    public static void main(String[] args) {
        String a = "hello";
    }
Constant pool:
  #17 = String             #18            // hello
  #18 = Utf8               hello

{
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
Error: unknown attribute
      org.aspectj.weaver.MethodDeclarationLineNumber: length = 0x8
       00 00 00 10 00 00 00 BA
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #17                 // String hello
         2: pop
         3: return
      LineNumberTable:
        line 17: 0
        line 18: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  args   [Ljava/lang/String;
}
SourceFile: "ClassPoolTest.java"

在这里0: ldc #17,#17对应的“hello”,如果字符串常量池中已经存在“hello”,java不会做什么事,但是如果字符串常量池中没有该字符串常量,java会把该字符串常量放到常量池中,并且把引用放到stringtable中。

 什么时候才会用到ldc指令

凡是有双引号括起字符串的地方就会用到ldc指令,比如上面的String a = "hello"。
我们再来看看几个例子:

第一个:

public class ClassPoolTest {
    String a = "hello";
    public static void main(String[] args) {}
}

 

我们执行完main以后,hello不会进入字符串常量池。因为String a = "hello"是ClassPoolTest 的成员变量,成员变量只有在执行到构造方法的时候才会初始化。不信我们来看构造函数的反编译代码:

 public pool.ClassPoolTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #10                 // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #12                 // String hello
         7: putfield      #14                 // Field a:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 13: 0
        line 14: 4
        line 13: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lpool/ClassPoolTest;

第二个:

public class ClassPoolTest {
    static String a = "hello";
    public static void main(String[] args) {}
}

 执行完main以后,hello会进入常量池,因为static String a = "hello"是ClassPoolTest 静态变量,我们执行静态方法main之后张初始化静态变量,如下

 static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #10                 // String hello
         2: putstatic     #12                 // Field a:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 14: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

 可见hello已经被添加到字符串常量池中。

 总结来说,可以结合类加载机制,在类加载后实例化之前,只初始化了静态成员变量,只有实例化后才会初始化普通成员变量。字面量的字符串是否被添加到常量池中,要取决于当前字符串有没有被初始化。

intern方法详解

intern的作用是把new出来的字符串的引用添加到stringtable中,java会先计算string的hashcode,查找stringtable中是否已经有string对应的引用了,如果有返回引用(地址)。如果没有,jdk1.7及以后把字符串的地址放到stringtable中,并返回字符串的引用(地址),jdk1.7以前是传创建一个相同的字符串对象放入到常量池中。

我们继续看例子:

第一个:

    public static void main(String[] args) {
        String a = new String("haha");
        System.out.println(a.intern() == a);//false
    }

因为有双引号括起来的字符串,所以会把ldc命令,即"haha"会被我们添加到字符串常量池,它的引用会被我们添加到stringtable中。所以a.intern的时候,返回的其实是常量池中字符串的引用,和a的string实例化地址肯定是不一样的。

 

第二个:

        String e = new String("jo") + new String("hn");
        System.out.println(e.intern() == e);//true

new String("jo") + new String("hn")实际上会转为StringBuilder的append 然后tosring()出来,实际上是new 一个新的string出来。在这个过程中,并没有双引号括起john,也就是说并不会执行ldc然后把john的引用添加到stringtable中,所以intern的时候实际就是把新的string地址(即e的地址)添加到stringtable中并且返回回来

第三个:

        String f = new String("ja") + new String("va");
        System.out.println(f.intern() == f);//false

或许很多朋友感觉很奇怪,这跟上面的例子2基本一模一样,但是却是false呢?这是因为java在启动的时候,会把一部分的字符串添加到字符串常量池中,而这个“java”就是其中之一。所以intern回来的引用是早就添加到字符串常量池中的”java“的引用,所以肯定跟f的原地址不一样。

第四个:

结合intern方法的机制,这里验证一下上面提到的,字符串何时被添加到常量池中。参照第二个例子的结果。看如下代码执行结果

    public static void main(String[] args) {
        String s1=new String("2")+new String("2");
        String s2=s1.intern();
        String s4 = "22";
        System.out.println(s4==s1);//true
    }

s1初始化创建一个字符串对象“22”,指向堆中的引用。由于最开始s4还没有初始化,所以常量池中没有“22”,所以s1.intern执行后,常量池StringTable中添加了s1的引用,并把引用返回给了s2。s4创建时常量池中已存在“22”,所以s4的引用为常量池中的引用,也就是s1的引用。所以结果为true。

    public static void main(String[] args) {
        String s4 = "22";
        String s1=new String("2")+new String("2");
        String s2=s1.intern();
        System.out.println(s2==s1);//false
        System.out.println(s2==s4);//true
    }

s4初始化创建字符串“22”,并添加到常量池中,引用添加到StringTable中。s1初始化创建一个字符串对象“22”,指向堆中的引用。s1.intern先查找常量池中是否存在“22”,发现存在,直接返回常量池中对象的引用,赋值给了s2。所以s2与s1不是同一个对象,s2与s4是同一个对象。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值