先举个例子
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是同一个对象。