https://blog.csdn.net/soonfly/article/details/70147205
在翻《深入理解Java虚拟机》的书时,又看到了2-7的 String.intern()返回引用的测试。
其实要搞明白String.intern(),我总结了下面几条规则:
一、new String都是在堆上创建字符串对象。当调用 intern() 方法时,编译器会将字符串添加到常量池中(stringTable维护),并返回指向该常量的引用。
二、通过字面量赋值创建字符串(如:String str=”twm”)时,会先在常量池中查找是否存在相同的字符串,若存在,则将栈中的引用直接指向该字符串;若不存在,则在常量池中生成一个字符串,再将栈中的引用指向该字符串。
三、常量字符串的“+”操作,编译阶段直接会合成为一个字符串。如string str=”JA”+”VA”,在编译阶段会直接合并成语句String str=”JAVA”,于是会去常量池中查找是否存在”JAVA”,从而进行创建或引用。
四、对于final字段,编译期直接进行了常量替换(而对于非final字段则是在运行期进行赋值处理的)。
final String str1=”ja”;
final String str2=”va”;
String str3=str1+str2;
在编译时,直接替换成了String str3=”ja”+”va”,根据第三条规则,再次替换成String str3=”JAVA”
五、常量字符串和变量拼接时(如:String str3=baseStr + “01”;)会调用stringBuilder.append()在堆上创建新的对象。
六、JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。简单的说,就是往常量池放的东西变了:原来在常量池中找不到时,复制一个副本放到常量池,1.7后则是将在堆上的地址引用复制到常量池。
举例说明:
-
String str2 = new String("str")+new String("01");
-
str2.intern();
-
String str1 = "str01";
-
System.out.println(str2==str1);
在JDK 1.7下,当执行str2.intern();时,因为常量池中没有“str01”这个字符串,所以会在常量池中生成一个对堆中的“str01”的引用(注意这里是引用 ,就是这个区别于JDK 1.6的地方。在JDK1.6下是生成原字符串的拷贝),而在进行String str1 = “str01”;字面量赋值的时候,常量池中已经存在一个引用,所以直接返回了该引用,因此str1和str2都指向堆中的同一个字符串,返回true。
-
String str2 = new String("str")+new String("01");
-
String str1 = "str01";
-
str2.intern();
-
System.out.println(str2==str1);
将中间两行调换位置以后,因为在进行字面量赋值(String str1 = “str01″)的时候,常量池中不存在,所以str1指向的常量池中的位置,而str2指向的是堆中的对象,再进行intern方法时,对str1和str2已经没有影响了,所以返回false。
常见试题解答
有了对以上的知识的了解,我们现在再来看常见的面试或笔试题就很简单了:
Q:下列程序的输出结果:
String s1 = “abc”;
String s2 = “abc”;
System.out.println(s1 == s2);
A:true,均指向常量池中对象。
Q:下列程序的输出结果:
String s1 = new String(“abc”);
String s2 = new String(“abc”);
System.out.println(s1 == s2);
A:false,两个引用指向堆中的不同对象。
Q:下列程序的输出结果:
String s1 = “abc”;
String s2 = “a”;
String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:false,因为s2+s3实际上是使用StringBuilder.append来完成,会生成不同的对象。
Q:下列程序的输出结果:
String s1 = “abc”;
final String s2 = “a”;
final String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:true,因为final变量在编译后会直接替换成对应的值,所以实际上等于s4=”a”+”bc”,而这种情况下,编译器会直接合并为s4=”abc”,所以最终s1==s4。
Q:下列程序的输出结果:
String s = new String(“abc”);
String s1 = “abc”;
String s2 = new String(“abc”);
System.out.println(s == s1.intern());
System.out.println(s == s2.intern());
System.out.println(s1 == s2.intern());
A:false,false,true。
https://blog.csdn.net/SEU_Calvin/article/details/52291082
2.深入认识intern()方法
JDK1.7后,常量池被放入到堆空间中,这导致intern()函数的功能不同,具体怎么个不同法,且看看下面代码,这个例子是网上流传较广的一个例子,分析图也是直接粘贴过来的,这里我会用自己的理解去解释这个例子:
-
String s = new String("1");
-
s.intern();
-
String s2 = "1";
-
System.out.println(s == s2);
-
String s3 = new String("1") + new String("1");
-
s3.intern();
-
String s4 = "11";
-
System.out.println(s3 == s4);
输出结果为:
-
JDK1.6以及以下:false false
-
JDK1.7以及以上:false true
再分别调整上面代码2.3行、7.8行的顺序:
-
String s = new String("1");
-
String s2 = "1";
-
s.intern();
-
System.out.println(s == s2);
-
String s3 = new String("1") + new String("1");
-
String s4 = "11";
-
s3.intern();
-
System.out.println(s3 == s4);
输出结果为:
-
JDK1.6以及以下:false false
-
JDK1.7以及以上:false false
下面依据上面代码对intern()方法进行分析:
2.1 JDK1.6
在JDK1.6中所有的输出结果都是 false,因为JDK1.6以及以前版本中,常量池是放在 Perm 区(属于方法区)中的,熟悉JVM的话应该知道这是和堆区完全分开的。
使用引号声明的字符串都是会直接在字符串常量池中生成的,而 new 出来的 String 对象是放在堆空间中的。所以两者的内存地址肯定是不相同的,即使调用了intern()方法也是不影响的。如果不清楚String类的“==”和equals()的区别可以查看我的这篇博文Java面试——从Java堆、栈角度比较equals和==的区别。
intern()方法在JDK1.6中的作用是:比如String s = new String("SEU_Calvin"),再调用s.intern(),此时返回值还是字符串"SEU_Calvin",表面上看起来好像这个方法没什么用处。但实际上,在JDK1.6中它做了个小动作:检查字符串池里是否存在"SEU_Calvin"这么一个字符串,如果存在,就返回池里的字符串;如果不存在,该方法会把"SEU_Calvin"添加到字符串池中,然后再返回它的引用。然而在JDK1.7中却不是这样的,后面会讨论。
2.2 JDK1.7
针对JDK1.7以及以上的版本,我们将上面两段代码分开讨论。先看第一段代码的情况:
再把第一段代码贴一下便于查看:
-
String s = new String("1");
-
s.intern();
-
String s2 = "1";
-
System.out.println(s == s2);
-
String s3 = new String("1") + new String("1");
-
s3.intern();
-
String s4 = "11";
-
System.out.println(s3 == s4);
String s = newString("1"),生成了常量池中的“1” 和堆空间中的字符串对象。
s.intern(),这一行的作用是s对象去常量池中寻找后发现"1"已经存在于常量池中了。
String s2 = "1",这行代码是生成一个s2的引用指向常量池中的“1”对象。
结果就是 s 和 s2 的引用地址明显不同。因此返回了false。
String s3 = new String("1") + newString("1"),这行代码在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。
s3.intern(),这一行代码,是将 s3中的“11”字符串放入 String 常量池中,此时常量池中不存在“11”字符串,JDK1.6的做法是直接在常量池中生成一个 "11" 的对象。
但是在JDK1.7中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用直接指向 s3 引用的对象,也就是说s3.intern() ==s3会返回true。
String s4 = "11", 这一行代码会直接去常量池中创建,但是发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。因此s3 == s4返回了true。
下面继续分析第二段代码:
再把第二段代码贴一下便于查看:
-
String s = new String("1");
-
String s2 = "1";
-
s.intern();
-
System.out.println(s == s2);
-
String s3 = new String("1") + new String("1");
-
String s4 = "11";
-
s3.intern();
-
System.out.println(s3 == s4);
String s = newString("1"),生成了常量池中的“1” 和堆空间中的字符串对象。
String s2 = "1",这行代码是生成一个s2的引用指向常量池中的“1”对象,但是发现已经存在了,那么就直接指向了它。
s.intern(),这一行在这里就没什么实际作用了。因为"1"已经存在了。
结果就是 s 和 s2 的引用地址明显不同。因此返回了false。
String s3 = new String("1") + newString("1"),这行代码在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。
String s4 = "11", 这一行代码会直接去生成常量池中的"11"。
s3.intern(),这一行在这里就没什么实际作用了。因为"11"已经存在了。
结果就是 s3 和 s4 的引用地址明显不同。因此返回了false。
为了确保文章的实时更新,实时修改可能出错的地方,请确保这篇是原文,而不是无脑转载来的“原创文”,原文链接为:SEU_Calvin的博客。
3 总结
终于要做Ending了。现在再来看一下开篇给的引入例子,是不是就很清晰了呢。
-
String str1 = new String("SEU") + new String("Calvin");
-
System.out.println(str1.intern() == str1);
-
System.out.println(str1 == "SEUCalvin");
str1.intern() == str1就是上面例子中的情况,str1.intern()发现常量池中不存在“SEUCalvin”,因此指向了str1。 "SEUCalvin"在常量池中创建时,也就直接指向了str1了。两个都返回true就理所当然啦。
那么第二段代码呢:
-
String str2 = "SEUCalvin";//新加的一行代码,其余不变
-
String str1 = new String("SEU")+ new String("Calvin");
-
System.out.println(str1.intern() == str1);
-
System.out.println(str1 == "SEUCalvin");
也很简单啦,str2先在常量池中创建了“SEUCalvin”,那么str1.intern()当然就直接指向了str2,你可以去验证它们两个是返回的true。后面的"SEUCalvin"也一样指向str2。所以谁都不搭理在堆空间中的str1了,所以都返回了false。
https://blog.csdn.net/Sun1956/article/details/53161560/
new String()究竟创建几个对象?
1. 由来
遇到一个Java面试题,是关于String
的,自己对String
还有点研究?下面是题目的描述:
在Java中,
new String("hello")
这样的创建方式,到底创建了几个String
对象?
题目下答案,各说纷纭,有说1个的,有说2个的。我觉得都对,但也都不对,因为要加上一定的条件,下面来分析下!
2. 解答
2.1. 分析
题目中的String
创建方式,是调用String
的有参构造函数,而这个有参构造函数的源码则是这样的public String(String original)
,这就是说,我们可以把代码转换为下面这种:
-
-
String temp = "hello"; // 在常量池中
-
-
String str = new String(temp); // 在堆上
这段代码就创建了2个String
对象,temp
指向在常量池中的,str
指向堆上的,而str
内部的char value[]
则指向常量池中的char value[]
,所以这里的答案是2个对象。(这里不再详述内部过程,之前的文章有写,参考深入浅出Java String)
那之前我为什么说答案是1个的也对呢,假如就只有这一句String str = new String("hello")
代码,并且此时的常量池的没有"hello"
这个String,那么答案是两个;如果此时常量池中,已经存在了"hello"
,那么此时就只创建堆上str
,而不会创建常量池中temp
,(注意这里都是引用),所以此时答案就是1个。
https://blog.csdn.net/w605283073/article/details/72753494
《深入理解java虚拟机》第二版 57页
对String.intern()返回引用的测试代码如下:
-
/** String的intern例子
-
* Created by 明明如月 on 2017-05-24.
-
*/
-
public class RuntimeConstantPoolOOM {
-
public static void main(String[] args) {
-
String str1 = new StringBuilder("计算机").append("软件").toString();
-
// String str3= new StringBuilder("计算机软件").toString();
-
System.out.println(str1.intern() == str1);
-
String str2 = new StringBuilder("Java(TM) SE ").append("Runtime Environment").toString();
-
System.out.println(str2.intern() == str2);
-
}
-
}
结果是 :
true
false
可能很多人觉得这个结果很奇怪,在这里我们进行深入地探究。
书中写道,如果JDK1.6会返回两个false,JDK1.7运行则会返回一个true一个false。
因为JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串的实例的引用,而StringBulder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。
在JDK1.7中,intern()的实现不会在复制实例,只是在常量池中记录首次出现的实例引用,因此返回的是引用和由StringBuilder.toString()创建的那个字符串实例是同一个。
str2的比较返回false因为"java"这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串是首次出现,因此返回true。
那么就有疑问了,这个“java”字符串在哪里出现过呢?显然并不是直接出现在这个类里面。
我们分别打开String 、StringBuilder和System类的源码看看有啥发现,
其中在System类里发现
根据注释可以看出来,System是由虚拟机自动调用的。
在initializeSystemClass 方法中发现调用了Version对象的init静态方法
而Version类里 laucher_name是私有静态字符串常量
因此sun.misc.Version 类会在JDK类库的初始化过程中被加载并初始化,而在初始化时它需要对静态常量字段根据指定的常量值(ConstantValue)做默认初始化,此时被 sun.misc.Version.launcher 静态常量字段所引用的"java"字符串字面量就被intern到HotSpot VM的字符串常量池——StringTable里了。
因此我们修改一下代码:
-
String str2 = new StringBuilder("Java(TM) SE ").append("Runtime Environment").toString();
-
System.out.println(str2.intern() == str2)
发现结果还是false
从而更加证实了我们的猜测。
再遇到类似问题的时候,希望大家可以多从源码角度去追本溯源,能够多分享出来。