其余更多java基础文章: java基础学习(目录)html
学习资料:
String类API中文
深刻解析String#intern
Java 中new String("字面量") 中 "字面量" 是什么时候进入字符串常量池的?
new一个String对象的时候,若是常量池没有相应的字面量真的会去它那里建立一个吗?我表示怀疑。java
经过上一篇的学习,咱们已经了解了String源码的方法,这一章,咱们就经过Stirng.intern()方法来延伸,讲一下String的其余方面。git
字符串字面量是在 Java™语言规范的3.10.5. String 字面量中定义的 关于字面量通俗点解释就是,使用双引号""
建立的字符串,在堆中建立了对象后其引用插入到字符串常量池中(jdk1.7后),能够全局使用,遇到相同内容的字面量,就不须要再次建立。举个例子:github
//这就是建立了一个aaa字符串字面量 String a = "aaa"; //简单来讲,这就是建立了一个Stirng对象和一个aaa字符串字面量,后面会详细讨论 String a = new String("aaa") 复制代码
java中常量池的概念主要有三个:全局字符串常量池
,class文件常量池
,运行时常量池
。咱们如今所说的就是全局字符串常量池
,在下文中可能会简称常量池。对这个想弄明白的同窗能够看这篇Java中几种常量池的区分。bash
字符串常量池里面存的究竟是对象,仍是引用呢?我查了不少资料,最后根据本身的测试和查到的各类说法,认为在jdk1.7后字符串常量池中存的是引用。在new一个String对象的时候,若是常量池没有相应的字面量真的会去它那里建立一个吗?我表示怀疑。问题中,R大的回答解答了我:markdown
至于说: 以前一直有个结论就是:当建立一个string对象的时候,去字符串常量池看是否有相应的字面量,若是没有就建立一个。 这个说法历来都不正确。 对象在堆里。常量池存引用。oracle
这个字符串常量池的位置也是随着jdk版本的不一样而位置不一样。在jdk6中,常量池的位置在永久代(方法区)中,此时常量池中存储的是对象。在jdk7中,常量池的位置在堆中,此时,常量池存储的就是引用了。在jdk8中,永久代(方法区)被元空间取代了。这里就引出了一个很常见很经典的问题,看下面这段代码。app
@Test public void test(){ String s = new String("2"); s.intern(); String s2 = "2"; System.out.println(s == s2); String s3 = new String("3") + new String("3"); s3.intern(); String s4 = "33"; System.out.println(s3 == s4); } jdk6 false false jdk7 false true 复制代码
这段代码在jdk6中输出是false false
,可是在jdk7中输出的是false true
。咱们经过图来一行行解释。函数
JDK1.6 oop
String s = new String("2");
建立了两个对象,一个在堆中的StringObject对象,一个是在常量池中的“2”对象。
s.intern();
在常量池中寻找与s变量内容相同的对象,发现已经存在内容相同对象“2”,返回对象2的地址。
String s2 = "2";
使用字面量建立,在常量池寻找是否有相同内容的对象,发现有,返回对象"2"的地址。
System.out.println(s == s2);
从上面能够分析出,s变量和s2变量地址指向的是不一样的对象,因此返回false
String s3 = new String("3") + new String("3");
建立了两个对象,一个在堆中的StringObject对象,一个是在常量池中的“3”对象。中间还有2个匿名的new String("3")咱们不去讨论它们。
s3.intern();
在常量池中寻找与s3变量内容相同的对象,没有发现“33”对象,在常量池中建立“33”对象,返回“33”对象的地址。
String s4 = "33";
使用字面量建立,在常量池寻找是否有相同内容的对象,发现有,返回对象"33"的地址。
System.out.println(s3 == s4);
从上面能够分析出,s3变量和s4变量地址指向的是不一样的对象,因此返回false
JDK1.7
String s = new String("2");
建立了两个对象,一个在堆中的StringObject对象,一个是在堆中的“2”对象,并在常量池中保存“2”对象的引用地址。
s.intern();
在常量池中寻找与s变量内容相同的对象,发现已经存在内容相同对象“2”,返回对象“2”的引用地址。
String s2 = "2";
使用字面量建立,在常量池寻找是否有相同内容的对象,发现有,返回对象“2”的引用地址。
System.out.println(s == s2);
从上面能够分析出,s变量和s2变量地址指向的是不一样的对象,因此返回false
String s3 = new String("3") + new String("3");
建立了两个对象,一个在堆中的StringObject对象,一个是在堆中的“3”对象,并在常量池中保存“3”对象的引用地址。中间还有2个匿名的new String("3")咱们不去讨论它们。
s3.intern();
在常量池中寻找与s3变量内容相同的对象,没有发现“33”对象,将s3对应的StringObject对象的地址保存到常量池中,返回StringObject对象的地址。
String s4 = "33";
使用字面量建立,在常量池寻找是否有相同内容的对象,发现有,返回其地址,也就是StringObject对象的引用地址。
System.out.println(s3 == s4);
从上面能够分析出,s3变量和s4变量地址指向的是相同的对象,因此返回true。
再来一段变种代码
经过上面的逐句分析,应该都了解了为何两个版本的jdk返回值会不同了。那咱们稍稍改变一下上面代码中的语句顺序,将intern方法与字面量赋值语句调换顺序:
String s = new String("2"); String s2 = "2"; s.intern(); System.out.println(s == s2); String s3 = new String("3") + new String("3"); String s4 = "33"; s3.intern(); System.out.println(s3 == s4); 复制代码
答案是多少呢,你们能够稍微思考一下再往下看:
jdk6 false false jdk7 false false 复制代码
原理很简单,由于在调用intern方法前,先使用了字面量赋值语句,因此在常量池中都存在了与变量相同内容的对象(jdk1.6)或对象的引用(jdk1.7+),此时再调用intern方法,就会发现常量池里的对象地址和变量的地址不是指向同一个对象,天然就false了。对于这段不懂的同窗能够评论,我看需不须要再画一次结构图和逐句解释。
经过上面两段代码,咱们发现调用intern方法和字面量赋值的顺序是很重要的。咱们将上面两段代码都经过javap命令查看其字节码,发如今class类常量池中都有“33”。这说明在运行时,class常量池里的常量并不会直接所有加入到全局常量池中,那这是在何时加入的呢?我搜到了下面大神的回答 new String(“字面量”) 中 “字面量” 是什么时候进入字符串常量池的?
简单来讲:
HotSpot VM的实现来讲,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池 ;
在字面量赋值的时候,会翻译成字节码ldc指令,ldc指令触发lazy resolution动做
- 到当前类的运行时常量池(runtime constant pool,HotSpot VM里是ConstantPool + ConstantPoolCache)去查找该index对应的项
- 若是该项还没有resolve则resolve之,并返回resolve后的内容。
- 在遇到String类型常量时,resolve的过程若是发现StringTable已经有了内容匹配的java.lang.String的引用,则直接返回这个引用;
- 若是StringTable里还没有有内容匹配的String实例的引用,则会在Java堆里建立一个对应内容的String对象,而后在StringTable记录下这个引用,并返回这个引用出去。
在咱们使用中常常会用到+符号来拼接字符串,可是这个+符号在String中的实现仍是有讲究的。若是是相加含有String对象,则底部是使用StringBuilder实现的拼接的
String str1 ="str1"; String str2 ="str2"; String str3 = str1 + str2; 复制代码
若是相加的参数只有字面量或者常量或基础类型变量,则会直接编译为拼接后的字符串。
String str1 =1+"str2"+"str3"; 复制代码
这里有个小细节
若是使用字面量拼接的话,java常量池里是不会保存拼接的参数的,而是直接编译成拼接后的字符串保存,咱们看看这段代码:
String str1 = new String("aa"+"bb"); //String str3 = "aa"; String str2 = new StringBuilder("a").append("a").toString(); System.out.println(str2==str2.intern()); 复制代码
这段代码的输出是true
。能够得知,在str1变量的建立中,虽然咱们用了字面量“aa”,可是咱们常量池里并无aa,因此str2==str.intern()
才会返回true
。若是咱们去掉str3的注释,从新运行,就会输出false
。
我在学习的过程当中,遇到了一个疑问,怎么都查不到是为何,你们若是看到这里,能够顺手写一下这段代码,看是否是也会遇到这样的问题。
public static void main(String[] args){ String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4); } 复制代码
@Test public void test7(){ String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4); } 复制代码
如上所示,分别在test环境和main方法里运行相同代码,此时main函数里返回true
,test环境下倒是返回false
。按逻辑这里应该是返回true才对。可是我测试了将参数“1”改成“2“”或者“3”,二者返回的都是true。