• String.intern


    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类里发现

    《深入理解java虚拟机》String.intern()探究

    根据注释可以看出来,System是由虚拟机自动调用的。

    《深入理解java虚拟机》String.intern()探究

    在initializeSystemClass 方法中发现调用了Version对象的init静态方法

    《深入理解java虚拟机》String.intern()探究

    而Version类里 laucher_name是私有静态字符串常量

    《深入理解java虚拟机》String.intern()探究

    因此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

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

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

    如果有来生,要做一片树叶。 春天恋上枝,炎夏恋上水。 深秋恋上土,东来化作泥。 润物细无声,生生世世恋红尘。
  • 相关阅读:
    VC++用Recordset MSPersist载入C#DataSet Save出来的xml失败,但载入VC Recordset Save出来的xml则没问题,怎么xml不通用呢?
    观察力、分析问题的能力、与人沟通的能力和资源整合能力
    [导入]有感于神童之神源
    军训系列二:两类人创业不容易成功
    运行微软的SOAP3.0的VC样例Samples30_ADOTM_Client报错,m_pSoapClient>Invoke时直接失败
    About IM software
    [导入][转]好企业是什么样?
    动网论坛v7.0.0SQL版竟然帯病毒!
    CZoneSoft出品: 音频视频在线录制系列之 AV留言本 简介
    递归算法在生成树型结构中,几乎完全属于无稽的算法
  • 原文地址:https://www.cnblogs.com/shujiying/p/12333770.html
Copyright © 2020-2023  润新知