对于String相信大家已经都很熟悉了,在项目中出场率可以说是最高的。String不属于基本类型但它可以直接进行字面量赋值,如:String s = “adc”。而String字符串在面试题中也是出场率比较高的,当然深入了解String对我们也有一定的好处。
需要注意的是在JDK1.6 、JDK1.7、JDK1.8中,由于常量池的从永久代移到堆空间,导致String也有所改变。
移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除,譬如符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。在JDK1.8中已经不存在永久代的概念了。
字面量赋值
String s = “abc”;
通过字面量赋值创建字符串时,会优先在常量池中查找是否已经存在相同的字符串,倘若已经存在,栈中的引用直接指向该字符串;倘若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。
new对象生成字符串
String str = new String(”abc”)
通过new的方式创建字符串时,就直接在堆中生成一个字符串的对象,栈中的引用指向该对象。
当然上图是基于JDK1.6来说的。
intern()方法
intern()方法是在运行期将字符串添加到常量池中,在JDK1.6中,intern方法会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,如果不存在,则将字符串拷贝到常量池中并返回字符串的引用。而在JDK1.7后(包括JDK1.7),intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,如果不存在,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。
在了解了上面的知识后,我们再来看几个例子:
例一:
public class Test3 {
/**
* @param args
*/
public static void main(String[] args) {
String str1 = "string";
String str2 = new String("string");
String str3 = str2.intern();
System.out.println(str1==str2);
System.out.println(str1==str3);
}
}
运行结果:
false
true
对于str1指向的是常量池,而str2指向的是堆,所以是false
当调用intern()方法后,常量池中已经存在”string”,则直接返回”string”的引用,所以str1和str3是同一个引用,true。
例二:
public class Test3 {
/**
* @param args
*/
public static void main(String[] args) {
String baseStr = "baseStr";
final String baseFinalStr = "baseStr";
String str1 = "baseStr01";
String str2 = "baseStr"+"01";
String str3 = baseStr + "01";
String str4 = baseFinalStr+"01";
String str5 = new String("baseStr01").intern();
System.out.println(str1 == str2);//#3
System.out.println(str1 == str3);//#4
System.out.println(str1 == str4);//#5
System.out.println(str1 == str5);//#6
}
}
结果:
true
false
true
true
我们通过反编译来详细讲解。首先找到代码的位置,直接打开CMD命令窗口(ps:由于我装了cmd小工具,所以只要找到文件位置右键选择打开cmd窗口,当然也可以在桌面打开cmd然后通过命令找到位置cmd小工具,直接点击注册就行)
反编译命令:javac Test3.java
查看常量池 命令: javap -v Test3
部分片段
Constant pool:
#1 = Methodref #15.#28 // java/lang/Object."<init>":()V
#2 = String #29 // baseStr
#3 = String #30 // baseStr01
#4 = Class #31 // java/lang/StringBuilder
#5 = Methodref #4.#28 // java/lang/StringBuilder."<init>":(
)V
#6 = Methodref #4.#32 // java/lang/StringBuilder.append:(Lj
ava/lang/String;)Ljava/lang/StringBuilder;
#7 = String #33 // 01
#8 = Methodref #4.#34 // java/lang/StringBuilder.toString:(
)Ljava/lang/String;
#9 = Class #35 // java/lang/String
#10 = Methodref #9.#36 // java/lang/String."<init>":(Ljava/l
ang/String;)V
#11 = Methodref #9.#37 // java/lang/String.intern:()Ljava/la
ng/String;
#12 = Fieldref #38.#39 // java/lang/System.out:Ljava/io/Prin
tStream;
#13 = Methodref #40.#41 // java/io/PrintStream.println:(Z)V
#14 = Class #42 // com/test/Test3
#15 = Class #43 // java/lang/Object
#16 = Utf8 <init>
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 StackMapTable
#23 = Class #44 // "[Ljava/lang/String;"
#24 = Class #35 // java/lang/String
#25 = Class #45 // java/io/PrintStream
#26 = Utf8 SourceFile
#27 = Utf8 Test3.java
#28 = NameAndType #16:#17 // "<init>":()V
#29 = Utf8 baseStr
#30 = Utf8 baseStr01
#31 = Utf8 java/lang/StringBuilder
#32 = NameAndType #46:#47 // append:(Ljava/lang/String;)Ljava/l
ang/StringBuilder;
#33 = Utf8 01
#34 = NameAndType #48:#49 // toString:()Ljava/lang/String;
#35 = Utf8 java/lang/String
#36 = NameAndType #16:#50 // "<init>":(Ljava/lang/String;)V
#37 = NameAndType #51:#49 // intern:()Ljava/lang/String;
#38 = Class #52 // java/lang/System
#39 = NameAndType #53:#54 // out:Ljava/io/PrintStream;
#40 = Class #45 // java/io/PrintStream
#41 = NameAndType #55:#56 // println:(Z)V
#42 = Utf8 com/test/Test3
#43 = Utf8 java/lang/Object
#44 = Utf8 [Ljava/lang/String;
#45 = Utf8 java/io/PrintStream
#46 = Utf8 append
#47 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#48 = Utf8 toString
#49 = Utf8 ()Ljava/lang/String;
#50 = Utf8 (Ljava/lang/String;)V
#51 = Utf8 intern
#52 = Utf8 java/lang/System
#53 = Utf8 out
#54 = Utf8 Ljava/io/PrintStream;
#55 = Utf8 println
#56 = Utf8 (Z)V
执行指令(部分,第二列#+序数对应常量池中的项):可以执行命令:javap -c Test3
部分指令
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String baseStr
2: astore_1
3: ldc #3 // String baseStr01
5: astore_3
6: ldc #3 // String baseStr01
8: astore 4
10: new #4 // class java/lang/StringBuilder
13: dup
14: invokespecial #5 // Method java/lang/StringBuilder."<
init>":()V
17: aload_1
18: invokevirtual #6 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: ldc #7 // String 01
23: invokevirtual #6 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26: invokevirtual #8 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
29: astore 5
31: ldc #3 // String baseStr01
33: astore 6
35: new #9 // class java/lang/String
38: dup
39: ldc #3 // String baseStr01
41: invokespecial #10 // Method java/lang/String."<init>":
(Ljava/lang/String;)V
44: invokevirtual #11 // Method java/lang/String.intern:()
Ljava/lang/String;
47: astore 7
49: getstatic #12 // Field java/lang/System.out:Ljava/
io/PrintStream;
52: aload_3
53: aload 4
55: if_acmpne 62
58: iconst_1
59: goto 63
在解释上述执行过程之前,先了解两条指令:
ldc:Push item from run-time constant pool,从常量池中加载指定项的引用到栈。
astore_:Store reference into local variable,将引用赋值给第n个局部变量。
现在我们开始解释代码段二的执行过程:
0: ldc #2:加载常量池中的第二项(”baseStr”)到栈中。
2: astore_1 :将1中的引用赋值给第一个局部变量,即String baseStr = “baseStr”;
3: ldc #2:加载常量池中的第二项(”baseStr”)到栈中。
5: astore_2 :将3中的引用赋值给第二个局部变量,即 final String baseFinalStr=”baseStr”;
6: ldc #3:加载常量池中的第三项(”baseStr01”)到栈中。
8: astore_3 :将6中的引用赋值给第三个局部变量,即String str1=”baseStr01”;
9: ldc #3:加载常量池中的第三项(”baseStr01”)到栈中。
11: astore 4:将9中的引用赋值给第四个局部变量:即String str2=”baseStr01”;
结果#3:str1==str2 肯定会返回true,因为str1和str2都指向常量池中的同一引用地址。所以其实在JAVA 1.6之后,常量字符串的“+”操作,编译阶段直接会合成为一个字符串。
13: new #4:生成StringBuilder的实例。
16: dup :复制13生成对象的引用并压入栈中。
17: invokespecial #5:调用常量池中的第五项,即StringBuilder.方法。
以上三条指令的作用是生成一个StringBuilder的对象。
20: aload_1 :加载第一个参数的值,即”baseStr”
21: invokevirtual #6 :调用StringBuilder对象的append方法。
24: ldc #7:加载常量池中的第七项(”01”)到栈中。
26: invokevirtual #6:调用StringBuilder.append方法。
29: invokevirtual #8:调用StringBuilder.toString方法。
32: astore 5:将29中的结果引用赋值改第五个局部变量,即对变量str3的赋值。
结果 #4:因为str3实际上是stringBuilder.append()生成的结果,所以与str1不相等,结果返回false。(常量字符串和变量拼接时(如:String str3=baseStr + “01”;)会调用stringBuilder.append()在堆上创建新的对象)
34: ldc #3:加载常量池中的第三项(”baseStr01”)到栈中。
36: astore 6:将34中的引用赋值给第六个局部变量,即str4=”baseStr01”;
结果 #5 :因为str1和str4指向的都是常量池中的第三项,所以str1==str4返回true。这里我们还能发现一个现象,对于final字段,编译期直接进行了常量替换,而对于非final字段则是在运行期进行赋值处理的。
38: new #9:创建String对象
41: dup :复制引用并压如栈中。
42: ldc #3:加载常量池中的第三项(”baseStr01”)到栈中。
44: invokespecial #10:调用String.””方法,并传42步骤中的引用作为参数传入该方法。
47: invokevirtual #11:调用String.intern方法。
从38到41的对应的源码就是new String(“baseStr01”).intern()。
50: astore 7:将47步返回的结果赋值给变量7,即str5指向baseStr01在常量池中的位置。
结果 #6 :因为str5和str1都指向的都是常量池中的同一个字符串,所以str1==str5返回true。
例三:
public class Test3 {
/**
* @param args
*/
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
str1.intern();
String str ="计算机软件" ;
System.out.println( str == str1);
String str2 = new StringBuilder("ja").append("va").toString();
str2.intern();
String str3 = "java";
System.out.println(str3 == str2);
}
}
JDK1.6 :false,false JDK1.7 true ,false
对于JDK1.6来说好理解,一个指向的是常量池的引用一个是堆的引用,所以都是false,对于JDK1.7来说intern()方法不再复制实例,而只是在常量池中生成一个对原字符串的引用。因为常量池中没有“计算机软件”这个字符串,所以会在常量池中生成一个对堆中的“计算机软件”的引用,而在进行字面量赋值的时候,常量池中已经存在,所以直接返回该引用即可,因此str1和str2都指向堆中的字符串,返回true。,由于”java”在常量池中是早已经存在的(内置的),所以str3 和str2指向的不是同一个实例。
本文中的例子都是用==,==比较的是String对象地址,如果是equals则是比较的值,结果会与上面有差异(String重写了equals())
参考资料:http://www.cnblogs.com/paddix/p/5326863.html
https://blog.csdn.net/shanksqian/article/details/78330294
https://blog.csdn.net/soonfly/article/details/70147205