String详解

本文为转载作品,详细点击原文链接!
原文链接为: https://blog.csdn.net/xzjayx/article/details/103020029
         <!--一个博主专栏付费入口结束-->
        <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-d284373521.css">
                                    <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-d284373521.css">
            <div class="htmledit_views" id="content_views">
                                        <p>&nbsp; &nbsp; String做为Java开发中常用的类,弄懂它是非常有必要的,但是往往很多工作了几年从业人员,也并没有特别熟悉过,所以楼主总结一下String的常量池,以及intern()方法等。技术无止境,当然本文也有不足之处,欢迎大家在评论区指正。</p>

前言

本次代码使用 jdk 1.8版本,并且以下代码示例除了第一个写了main()方法,并且所有的示例分别独立运行 ,其余为了简洁做了缺省main()。在创建字符串分析的同时,都默认省略了栈中的句柄指向分析。进入正题之时,先科普几个知识点

  1. String源码里面标注为final修饰的类,是一个不可改变的对象,那平时用到字符串A+字符串B怎么改变了呢,其实这里有涉及到String的常量池,首先常量池存放在方法区。
  2. 在jdk1.6时,方法区是存放在永久代(java堆的一部分,例如新生代,老年代)而在jdk1.7以后将字符串常量池移动到了的堆内存中  
  3. 在jdk1.8时,HotspotVM正式宣告了移除永久代,取而代之的是元数据区,元数据区存放在内存里面(存放一些加载class的信息),但是常量池还是和jdk1.7存放位置一样还是存放在堆中。

先看一波常见面试题:

    首先看一道常见的面试题,问输出的是什么?


 
 
  1. public static void main(String[] args){
  2. String s1 = new String( "123");
  3. String s2 = "123";
  4. System.out.println(s1 == s2);
  5. }

   基本上大家都能知道是false,但是再这么深究一次,问 String s1 = new String("123") 创建了几个对象,String s2 = "123" 创建  了几个对象,那如果题目稍微改变一下成下面这样,那输出的又是什么?


 
 
  1. String s1 = new String( "123").intern();
  2. String s2 = "123";
  3. System.out.println(s1 == s2); // true
  4. // 如果这样再改一下
  5. String s1 = new String( "123");
  6. s1.intern();
  7. String s2 = "123";
  8. System.out.println(s1 == s2); // false

  如果对输出结果不是很明白的,本文都会一一解答并且进行拓展。

创建字符串分析:

   首先要分析String,一定要知道String几种常见的创建字符串的方式,以及每一种不同的方式常量池和堆分别是什么储存情况。

    1.直接写双引号常量来创建 

       判断这个常量是否存在于常量池,
  如果存在,则直接返回地址值(只不过地址值分为两种情况,1是堆中的引用,2是本身常量池的地址)
    如果是引用,返回引用地址指向的堆空间对象地址值
    如果是常量,则直接返回常量池常量的地址值,
  如果不存在,
    在常量池中创建该常量,并返回此常量的地址值


 
 
  1. String s = "123";
  2. //true,因为s已经在常量池里面了,s.intern()返回的也是常量池的地址,两者地址一样为true
  3. System.out.println(s == s.intern());

    2. new String创建字符串

        与上面第一种方式相比,第一种方式效率高,下图解决了本文中的最开始出的部分面试题。

       首先在堆上创建对象(无论堆上是否存在相同字面量的对象),
     然后判断常量池上是否存在字符串的字面量,
       如果不存在
       在常量池上创建常量(并将常量地址值返回)
      如果存在
       不做任何操作


 
 
  1. String s = new String( "123");
  2. /*
  3. 严格来说首先肯定会在堆中创建一个123的对象,然后再去判断常量池中是否存在123的对象,
  4. 如果不存在,则在常量池中创建一个123的常量(与堆中的123不是一个对象),
  5. 如果存在,则不做任何操作,解决了本文第一个面试题有问到创建几个对象的问题。
  6. 因为常量池中是有123的对象的,s指向的是堆内存中的地址值,s.intern()返回是常量池中的123的常量池地址,所以输出false
  7. */
  8. System.out.println(s == s.intern());

   3.两个双引号的字符串相加

     判断这两个常量、相加后的常量在常量池上是否存在
  如果不存在
   则在常量池上创建相应的常量(并将常量地址值返回)
  如果存在,则直接返回地址值(只不过地址值分为两种情况,1是堆中的引用,2是本身常量池的地址)
    如果是引用,返回引用地址指向的堆空间对象地址值,
    如果是常量,则直接返回常量池常量的地址值,


 
 
  1. String s1 = new String( "123").intern();
  2. String s2 = "1"+ "23";
  3. /*
  4. * 首先第一句话 String s1 = new String("123") 以上分析过创建了两个对象(一个堆中,一个常量池 中)此时s1指向堆中
  5. * 当s1调用.intern()方法之后,发现常量池中已经有了字面量是123的常量,则直接把常量池的地址返回给s1
  6. * 在执行s2等于123时候,去常量池查看,同上常量池已经存在了,则此时s2不创建对象,直接拿常量池123的地址值使用
  7. * 所以此时s1 和 s2 都代表是常量池的地址值,则输出为true
  8. */
  9. System.out.println(s1 == s2);

    如果这里看不懂 intern()方法时,可以快速滑动到文章尾部,先看intern()方法的分析。 

  4.两个new String()的字符串相加

    首先会创建这两个对象(堆中)以及相加后的对象(堆中)
 然后判断常量池中是否存在这两个对象的字面量常量
  如果存在
   不做任何操作
  如果不存在
   则在常量池上创建对应常量     


 
 
  1. String s1 = new String( "1")+ new String( "23");
  2. /*
  3. * 首先堆中会有 1 ,23 ,以及相加之后的123 这三个对象。如果 1,23 这两个对象在常量池中没有相等的字面量
  4. * 那么还会在常量池中创建2个对象 最大创建了5个对象。最小创建了3个对象都在堆中。
  5. */
  6. s1.intern();
  7. String s2 = "123";
  8. System.out.println( s1 == s2); // true

   这个地方比较复杂 ,如果我把String s2 = "123" 代码放在s1.intern()前面先执行,其余代码不变,那么输出结果又为false,这里等会楼主会在分析 intern()方法的时候再重点分析一次。


 
 
  1. String s2 = "123";
  2. s1.intern();
  3. System.out.println( s1 == s2); // false

 5.双引号字符串常量与new String字符串相加

   首先创建两个对象,一个是new String的对象(堆中),一个是相加后的对象(堆中)
 然后判断双引号字符串字面量和new String的字面量在常量池是否存在
  如果存在
   不做操作
  如果不存在
   则在常量池上创建对象的常量


 
 
  1. String s1 = "1"+ new String( "23");
  2. /*
  3. *首先堆中会有 23 ,以及相加之后的123 这2个对象。如果23,1 这两个对象在常量池中没有相等的字面量
  4. *那么还会在常量池中创建2个对象最大创建了4个对象(2个堆中,2个在常量池中)。最小创建了2个对象都堆中。
  5. */
  6. String s2 = "123";
  7. System.out.println( s1.intern() == s2); // true

  6.双引号字符串常量与一个字符串变量相加

    首先创建一个对象,是相加后的结果对象(存放堆中,不会找常量池)
 然后判断双引号字符串字面量在常量池是否存在
  如果存在
   不做操作
  如果不存在
   则在常量池上创建对象的常量


 
 
  1. String s1 = "23";
  2. /*
  3. * 这里执行时,常量“1” 会首先到字符串常量池里面去找,如果没有就创建一个,并且加入字符串常量池。
  4. * 得到的123结果对象,不会存入到常量池。这里特别注意和两个常量字符串相加不同 “1”+“23” 参考上面第三点
  5. * 由于不会进入常量池,所以s2 和 s3 常量池地址值不同,所以输出为false
  6. */
  7. String s2 = "1"+s1;
  8. String s3 = "123";
  9. System.out.println( s2 == s3.intern());

   Q: 有人会问为什么两个常量字符串相加得到的对象就会入常量池(参考上面第3点),而加上一个变量就不会???

   A:  这是由于Jvm优化机制决定的,Jvm会有编译时的优化,如果是两个常量,Jvm会认定这已经是不可变的,就会直接在编译           时和常量池进行判断比对等,但是如果是加上一个变量,说明最后运行得出的结果是可变的,Jvm无法在编译时就确定执             行之后的结果是多少,所以不会把该结果和常量池比对。

String.intern()方法分析:

      在分析intern()方法时候,首先去官网查看api的相关解释

 楼主大概翻译一下,意思就是:当调用这个方法时候,如果常量池包含了一个<调用 code equals(Object)>相等的常量,就把该 常量池的对象返回,否则,就把当前对象加入到常量池中并且返回当前对象的引用。楼主用更加白话的方式解释一下:

 判断这个常量是否存在于常量池。
  如果存在,则直接返回地址值(只不过地址值分为两种情况,1是堆中的引用,2是本身常量池的地址)
    如果是引用,返回引用地址指向的堆空间对象地址值
    如果是常量,则直接返回常量池常量的地址值,
  如果不存在,
   将当前对象引用复制到常量池,并且返回的是当前对象的引用(这个和上面最开始的字符串创建分析有点不同)

实战分析问题:

         基本上读者看到这里就可以尝试着去回过头文章一些示例代码,看看输出结果,这里分析一下上文存在的一个例子


 
 
  1. public static void main(String[] args){
  2. String s1 = new String( "1")+ new String( "23");
  3. s1.intern();
  4. String s2 = "123";
  5. System.out.println( s1 == s2);
  6. }

    分析: 1 首先看第一行是两个new String类型的字符串相加(详见上文第4点)可知道,这里创建了堆中有3个对象 一个是1,                   一个是23,还有一个是结果 123,由于程序刚启动常量池也没有 1,23 所以会在常量池创建2个对象 (1 , 23)

                2 当s1执行intern()方法之后,首先去常量池判断有没有123,此时发现没有,所以会把对象加入到常量池,并且返回                       当前对象的引用(堆中的地址)

                3 当创建s2时候(详见上文第1点),并且找到常量池中123,并且把常量池的地址值返回给s2

                4 由于常量池的地址值就是s1调用intern()方法之后得到的堆中的引用,所以此时s1和s2的地址值一样,输出true。


 
 
  1. public static void main(String[] args){
  2. String s1 = new String( "1")+ new String( "23");
  3. String s2 = "123";
  4. s1.intern();
  5. System.out.println( s1 == s2);
  6. }

  如果把中间两行换一个位置,那输出就是false了,下面在分析一下不同点,上面分析过的不再赘述。

   1.在执行到第二行的时候String s2 = "123"时,发现常量池没有123,所以会先创建一个常量

   2.在当s1调用intern()方法时,会发现常量池已经有了123对象,就会直接把123的常量给返回出去,但是由于返回值并没有接          收,所以此时s1还是堆中地址,则输入false;如果代码换成  s1 = s1.intern();那s1就会重新指向常量池了,那输出就为true;

结尾:

      由于本文都是在Jdk1.8版本(1.7由于已经把常量池放在堆中了和1.8结果应该一样)执行,如果有读者要探究1.6的相关问题,主要知道jdk1.6在创建String和1.8(1.7)的堆和常量池有一些不同实现,那相关问题就很清楚了,这里楼主没有涉及到1.6的相关问题,以免混淆读者,如果需要探究1.6以及相关问题,欢迎在评论区留言。

      楼主很多时候都说了句柄指针的概念相对抽象,如果读者想知道两个对象是否是指向了相同的地址,可用   System.identityHashCode(Object x) 来验证。

       最后本文也肯定有一些不足之处,欢迎大家在评论区留言,学无止境,大家加油。(如需转载,请标注出处,谢谢!)

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值