先让大家做个面试题:
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里。
怎么样才能进入字符串常量池
往细讲,只有执行了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."":()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对应的引用了,如果有返回引用(地址),然后没有把字符串的地址放到stringtable中,并返回字符串的引用(地址)。
我们继续看例子:
public static void main(String[] args) {
String a = new String("haha");
System.out.println(a.intern() == a);//false
}
因为有双引号括起来的字符串,所以会把ldc命令,即"haha"会被我们添加到字符串常量池,它的引用是string的char数组的地址,会被我们添加到stringtable中。所以a.intern的时候,返回的其实是string中的char数组的地址,和a的string实例化地址肯定是不一样的。
String e = new String("jo") + new String("hn");
System.out.println(e.intern() == e);//true
new String("jo") + new String("hn")实际上会转为stringbuffer的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的原地址不一样。