码迷,mamicode.com
首页 > 其他好文 > 详细

String.intern

时间:2020-02-19 23:48:23      阅读:86      评论:0      收藏:0      [点我收藏+]

标签:常见   uil   笔试题   dash   sso   规则   euc   oom   记录   

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后则是将在堆上的地址引用复制到常量池。
技术图片

举例说明:

  1.  
    String str2 = new String("str")+new String("01");
  2.  
    str2.intern();
  3.  
    String str1 = "str01";
  4.  
    System.out.println(str2==str1);

在JDK 1.7下,当执行str2.intern();时,因为常量池中没有“str01”这个字符串,所以会在常量池中生成一个对堆中的“str01”的引用(注意这里是引用 ,就是这个区别于JDK 1.6的地方。在JDK1.6下是生成原字符串的拷贝),而在进行String str1 = “str01”;字面量赋值的时候,常量池中已经存在一个引用,所以直接返回了该引用,因此str1和str2都指向堆中的同一个字符串,返回true。

  1.  
    String str2 = new String("str")+new String("01");
  2.  
    String str1 = "str01";
  3.  
    str2.intern();
  4.  
    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()函数的功能不同,具体怎么个不同法,且看看下面代码,这个例子是网上流传较广的一个例子,分析图也是直接粘贴过来的,这里我会用自己的理解去解释这个例子:

 
  1. String s = new String("1");

  2. s.intern();

  3. String s2 = "1";

  4. System.out.println(s == s2);

  5.  
  6. String s3 = new String("1") + new String("1");

  7. s3.intern();

  8. String s4 = "11";

  9. System.out.println(s3 == s4);

输出结果为:

 
  1. JDK1.6以及以下:false false

  2. JDK1.7以及以上:false true

再分别调整上面代码2.3行、7.8行的顺序:

 
  1. String s = new String("1");

  2. String s2 = "1";

  3. s.intern();

  4. System.out.println(s == s2);

  5.  
  6. String s3 = new String("1") + new String("1");

  7. String s4 = "11";

  8. s3.intern();

  9. System.out.println(s3 == s4);

输出结果为:

 

 
  1. JDK1.6以及以下:false false

  2. 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以及以上的版本,我们将上面两段代码分开讨论。先看第一段代码的情况:

技术图片

 

再把第一段代码贴一下便于查看:

 
  1. String s = new String("1");

  2. s.intern();

  3. String s2 = "1";

  4. System.out.println(s == s2);

  5.  
  6. String s3 = new String("1") + new String("1");

  7. s3.intern();

  8. String s4 = "11";

  9. 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。

 

下面继续分析第二段代码:

技术图片

再把第二段代码贴一下便于查看:

 
  1. String s = new String("1");

  2. String s2 = "1";

  3. s.intern();

  4. System.out.println(s == s2);

  5.  
  6. String s3 = new String("1") + new String("1");

  7. String s4 = "11";

  8. s3.intern();

  9. 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了。现在再来看一下开篇给的引入例子,是不是就很清晰了呢。

 

 
  1. String str1 = new String("SEU") + new String("Calvin");

  2. System.out.println(str1.intern() == str1);

  3. System.out.println(str1 == "SEUCalvin");

 

str1.intern() == str1就是上面例子中的情况,str1.intern()发现常量池中不存在“SEUCalvin”,因此指向了str1。 "SEUCalvin"在常量池中创建时,也就直接指向了str1了。两个都返回true就理所当然啦。

那么第二段代码呢:

 

 
  1. String str2 = "SEUCalvin";//新加的一行代码,其余不变

  2. String str1 = new String("SEU")+ new String("Calvin");

  3. System.out.println(str1.intern() == str1);

  4. 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),这就是说,我们可以把代码转换为下面这种:

  1.  
     
  2.  
    String temp = "hello"; // 在常量池中
  3.  
     
  4.  
    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()返回引用的测试代码如下:

 

 
  1. /** String的intern例子

  2. * Created by 明明如月 on 2017-05-24.

  3. */

  4. public class RuntimeConstantPoolOOM {

  5. public static void main(String[] args) {

  6. String str1 = new StringBuilder("计算机").append("软件").toString();

  7. // String str3= new StringBuilder("计算机软件").toString();

  8. System.out.println(str1.intern() == str1);

  9. String str2 = new StringBuilder("Java(TM) SE ").append("Runtime Environment").toString();

  10. System.out.println(str2.intern() == str2);

  11. }

  12. }

 

结果是 :

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里了。

因此我们修改一下代码:

 
 
  1. String str2 = new StringBuilder("Java(TM) SE ").append("Runtime Environment").toString();

  2. System.out.println(str2.intern() == str2)

 

发现结果还是false

从而更加证实了我们的猜测。

再遇到类似问题的时候,希望大家可以多从源码角度去追本溯源,能够多分享出来。

技术图片

String.intern

标签:常见   uil   笔试题   dash   sso   规则   euc   oom   记录   

原文地址:https://www.cnblogs.com/shujiying/p/12333770.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!