JDK1.8 的String你真的了解么?

408 阅读15分钟

前言


所有验证基于String源码注解以及反射修改String 的值,String的intern()方法 文中出现的问题错误烦请大家指出~谢谢🙏



一 . 这些String在JVM中构建的区别你都清楚了么


  • 答案见文末

 很多博客中只分析结果,并没有通过String源码来看来分析,导致越传越歪。以下列子中我们只列举一下我从String源码中理解到的一些不同~~

  • 这里是String的拼接~

      String  var1 = new String("a") + new String("b");
      String  var2 = new String("a") + new String("b");
      // 这里肯定是ture啦~
      System.out.println(var1.intern() == var2.intern() );
    
      String var3 = "ab";
      // 但是下面这几个呢?
      System.out.println(var3 == var1);
      System.out.println(var3 == var2);
      System.out.println(var3 == var2.intern());
      
      // 这样又是什么结果呢?
      System.out.println(var1 == var2.intern());
      System.out.println(var1.intern() == var2);
    
  • new String(String original) 与 new String(char value[]) 区别在哪里?

        String var1 = new String("a");
        char ch[] = {'a'};
        String var2 = new String(ch);

        System.out.println(var1 +"++"+ var2);
        System.out.println(var1 == var2);
        System.out.println(var1 == var2.intern());
        System.out.println(var1.intern() == var2);
        System.out.println(var1.intern() == var2.intern());
  • 这里我们换一下顺序来看看~
        char ch[] = {'a'};
        String var2 = new String(ch);
        var2.intern();
        String var1 = "a";
        System.out.println(var1 == var2);
  • 那我们在进一步 看看如果我们通过反射修改了 String.value(这些后面都会讲到慢慢看哦) var1 var2的值会有什么变化呢
        String var1 = new String("a"); 
        char ch[] = {'a'};
        String var2 = new String(ch);
        var2.intern();
        System.out.println(var1 +"++"+ var2);// 这里是多少呢?
        // 这时我们开始修改var2的值啦~~~
        // 反射修改String 的值,看是否是共用关系
        Field field = String.class.getDeclaredField("value");
        field.setAccessible(true); // 因为 char 为 private,所以设置 char value 为可以访问,修改
        char[] char1 = (char[]) field.get(var2);
        char1[0] = 'b';// 修改var2的值为b
        System.out.println(var1+"+++"+var2);// 这里改变了么?
  • 现在我们做一些改变~~先来加载var2
        char ch[] = {'a'};
        String var2 = new String(ch);
        var2.intern();
        String var1 = new String("a");
        System.out.println(var1 +"++"+ var2);// 这里是多少呢?
        // 这时我们开始修改var2的值啦~~~
        // 反射修改String 的值,看是否是共用关系
        Field field = String.class.getDeclaredField("value");
        field.setAccessible(true); // 因为 char 为 private,所以设置 char value 为可以访问,修改
        char[] char1 = (char[]) field.get(var2);
        char1[0] = 'b';// 修改var2的值为b
        System.out.println(var1+"+++"+var2);// 这里改变了么?

 举例就到这里啦!如果有点蒙是正常的啦~耐心看完后续的讲解相信你也一定会醍醐灌顶~~

二 . JVM(jdk 1.8)

 在jdk 1.8之前还存在永久代,1.8对其进行移除,加入元空间概念。 为了方便说明简化了JVM,留下我们需要用到的JVM部分。

这里基于jdk 1.8 的JVM简化模型对String进行讲解,并不需要太多JVM知识哦~



三 . String的几种不同的构造方法究竟有何异同?

1 . String的两种最基本的构造方式

从最简单的开始 这样会很好理解


// 首先直接对String A 进行赋值时,栈中保存A的引用,引用指向常量池中“String对象”,不在堆中创建其他对象,存储方式如下

String var1 = "a";

// new创建对象时,存储方式发生了改变,栈中依然有A的引用,但是此时引用是指向堆中的A对象(堆中的对象A.value
// 与池中String value 引用相同,是同一个value 后面会继续讲)      

String var1 = new String("a");

上述是两种String构造方法,其实关键部分就是这个String对象的value,我们接下来就需要通过String源码和反射来看我们的模型画的是否正确啦。

2 . 通过String源码进行分析

   /* The value is used for character storage. */
    private final char value[];
    
    /**
     * Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument. in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.
     *
     
      换句话说,新创建的string是传入参数string的复制。如果我们不需要用到这个复制的string,用这个构造方法是没有必要的。
      (后两句话的翻译)
      
     * @param  original
     *         A {@code String}
     */
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

 通过上述源码不难发现,String对象中final char value[]是String用来储存值的(final是引用不可改变,但是值还是可以更改的,虽然是private属性,但是我们依然可以用反射来修改,后面验证会用到)。

 当我们用此构造方法传入String 对象参数时,其实是在常量池中创建了一个String对象,然后将这个String对象的value的值赋给new出的堆中的String(其实是赋值value对象的引用),也就是注释所说的是传入String的copy。

3 . 验证两种基本的String构造方法

       String var1 = "a";
       String var2 = "a";

  • 为什么这么这两个var1 var2 指向同一个常量池中的对象呢?首先是出于性能,可以共用,其次就是对java反汇编如下
    public static void main(String[] args) {
       String var1 = "a";
       String var2 = "a";
    }
    // 汇编如下
    public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String a // 从常量池中取得 a 压入栈顶
       2: astore_1                                      // 将栈顶对象的地址压入局部变量表
       3: ldc           #2                  // String a // 从常量池中取得 a 压入栈顶
       5: astore_2                                      // 将栈顶对象的地址压入局部变量表
       6: return
       
    我们不难发现,都是从常量池中取得 String a 压入栈。那么这个a是不是一个呢?
  • 通过上面反汇编与java 出于性能的设计,我们是不是可以猜测,var1 var2是指向同一个对象?验证如下

var1,var2指向常量池中同一个String ,那么我们就可以通过反射取得var2的value并修改,如果var1的值也被修改,那就可以证明 var1 var2指向常量池的同一个String;

       String var1 = "a";
       String var2 = "a";
 
       System.out.println(var1 == var2);// true
       System.out.println(var1+"+++"+var2);// 输出a+++a
 
        // 反射修改String 的值,看是否是共用关系
       Field field = String.class.getDeclaredField("value");
       field.setAccessible(true); // 因为value属性被private修饰,所以设置value 为可以访问修改 
       char[] char1 = (char[]) field.get(var2);// 取得var2 value的值
       char1[0] = 'b';// 修改var2的值为b
        
       System.out.println(var1 == var2);// true
       System.out.println(var1+"+++"+var2);// b+++b

结果如我们预料的一样,两个值均被改变。也就证明来var1,var2是指向同一个常量池中的String。也就是说通过这种方式构造String时会在常量池中比对,如果有会返回已经存在的常量池中String的地址


  • 在验证new Stirng 之前我们需要先讲一下String.intern()

这里先解释一下intern方法。从注释可以看出,如果常量池中存在相同的返回的是常量池中的地址,如果常量池中不存在,那么就把对象的地址加入常量池中并返回。注意这里不存在的时候返回的是堆中的对象的地址,常量池中加入的也是堆中对象的地址,而不是“abc”这样的值。 这里先用到的是常量池中存在的情况,不存在的后面会详细为大家解答。

     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * 

   当intern方法被调用。如果常量池中用equals方法比对已经存在相同的对象,那么这个常量池中的String被返回。否则,这个调用者(object)将被加入常量池中,并且这个对象的引用被返回。
  
  认真看的小伙伴这里应该会有疑惑,对象加入常量池中的,是一个对象还是对象的引用?(后面会验证的~)  

     <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */
    public native String intern();

通过上面的注解相信我们已经对intern方法有了大致的了解。下面让我们正式开始验证new String

  • 验证new String
    public static void main(String[] args) {
       String var1 = "a";
       String var2 = new String("a");
    }
  反汇编如下
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String a
       2: astore_1
       3: new           #3                  // class java/lang/String
       6: dup
       7: ldc           #2                  // String a
       9: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      12: astore_2
      13: return
    这个跟上面一样,这里不做过多讲解。我们可以看到,在执行init
    之前会将常量池中 String a 压入栈顶让init操作。那么是不是可以才想这时候是在对常量池中String进行复制呢?

上图中特意强调了value地址,并且常量池中的跟堆中的value地址是相同的,也就是说指向一个char。但是堆中的对象并不指向常量池。如果value地址都是相同的,那么我们修改var2的value,也就是堆中的对象的value,那么var1的value也会发生变化!那么就验证了我们的猜测。

补充验证.由上面对intern()的介绍可知,new String()的intern返回的应该是常量池中String的地址。那么var1 与 var2 必然是相同的。

       String var1 = "a";
       String var2 = new String("a");
 
       System.out.println(var1 == var2);//false
       System.out.println(var1 == var2.intern());//true
       System.out.println(var1+"+++"+var2);//输出a+++a
 
        //反射修改String 的值,看是否是共用关系
       Field field = String.class.getDeclaredField("value");
       field.setAccessible(true); //因为 char 为 private,所以设置 char value 为可以访问,修改
       char[] char1 = (char[]) field.get(var2);
       char1[0] = 'b';//修改var2的值为b
 
       System.out.println(var1 == var2);//false
       System.out.println(var1 == var2.intern());//true
       System.out.println(var1+"+++"+var2);//b+++b

至此String的两种基本的构造方法我们就讲完了


4 . 验证用传入char的String构造方法

  • 依照惯例我们先来看一下String的源码
   /**
     * Allocates a new {@code String} so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     创建一个包含传入char的String。这个char是被复制的,修改char并不会影响String的值。
     * @param  value
     *         The initial value of the string
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }

通过源码跟注释,我们可以发现用char构建String的时候是copy char并没有生成其他的String,那么此时是不是应该只在堆中生成一个String对象,常量池中并不存在。

  • 验证第一步:String直接赋值,String 传入char构建,修改其中value,另一个不变。如下。
       String var1 = "a";
       char[] cahra = {'a'};
       String var2 = new String(cahra);
 
       System.out.println(var1 == var2);//false
 
        //反射修改String 的值,看是否是共用关系
       Field field = String.class.getDeclaredField("value");
       field.setAccessible(true); //因为 char 为 private,所以设置 char value 为可以访问,修改
       char[] char1 = (char[]) field.get(var2);
       char1[0] = 'b';//修改var2的值为b
        
       System.out.println(var1+"+++"+var2);//a+++b

这里要注意,图中的堆中的String.value 与 常量池中的String.value值虽然相同,但是并不指向同一个char。也就是说堆中的String的char只有堆中String自己可以访问的到。

  • 验证第二部:我们通过Stirng.intern再次验证
        char ch[] = {'a'};
        String var2 = new String(ch); // 先创建,这时String只在堆中创建了一个对象。
        var2.intern();// 因为在常量池中没有找到相等的对象,所以这时将自己的引用加入了常量池中,返回的是堆中Stirng的地址,跟插入常量池中的一致。是个地址!!再强调一下
        String var1 = "a";// 这时会先去常量池中寻找,通过引用找到了堆中的String。
        
        System.out.println(var1 == var2);// true      证明var1 与 var2 地址一致。也就是说var1 var2 指向同一个String对象,也就是堆中的这个String对象。也证明了,常量池中存在的的确是堆中Stirng对象的地址引用!!!!
       
        Field field = String.class.getDeclaredField("value");
        field.setAccessible(true); //因为 char 为 private,所以设置 char value 为可以访问,修改
        char[] char1 = (char[]) field.get(var2);
        char1[0] = 'b';//修改var2的值为b

        System.out.println(var1 + var2);//bb

当intern发现常量池中不存在,就会将堆中的String地址存入常量池中,在创建值相同的String时就会通过地址找到堆中的这个对象。 所以也就会出现上面 var1 == var2 !! (要先intern哦~~)(new String("a").intern()不会这样的哦~原因上面说的很清楚啦~)

四 . String对象的拼接是如何进行?

大家上面关于传入char构建String 如果理解了的话。对于String对象拼接的原理理解起来就太简单了!!

  • 先看最简单的
      String var1 = "a" + "b";
      反汇编如下
      Code:
       0: ldc           #2        // String ab
       2: astore_1
       3: return
  

"a" + "b" 编译时会变成 "ab",这种情况已经讲解过啦我们来看下一种。


        String var1 = new String("b") + "a";
        String var2 = new String("b") + new String("a");
        
        String var3 = "b";
        String var4 = var3 + new String("a");
        

对于以上三种情况其实都是一样的,因为存在对象的引用时,并不能通过编译让他们直接拼接,这时就引入了另一个对象去拼接字符串--StringBuilder!

  String var1 = new String("a") + new String("b");
  
  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/StringBuilder
       3: dup
       
       // new StringBuilder 
       4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
       7: new           #4                  // class java/lang/String
      10: dup
      11: ldc           #5                  // String a
      13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      // 调用StringBuilder对象的append()方法拼接"a"
      16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: new           #4                  // class java/lang/String
      22: dup
      23: ldc           #8                  // String b
      25: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      28: invokevirtual #7                  // Method
      // 再次调用append()方法拼接"b"
      java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      // 最后我们可以清楚的看到,调用了StringBuilder的toString()方法!
      31: invokevirtual #9                  // Method
      java/lang/StringBuilder.toString:()Ljava/lang/String;
      34: astore_1
      35: return

从上面反汇编,我们可以看出,对象拼接是调用了StringBuilder对象的append方法进行拼接。最后用toString输出对象。

  • 在JVM中的存储又是什么样子的呢?

我们看到构建的堆中的"a" "b" 还有常量池中的"a" "b"这些都跟我们前面所讲的new String()完全一致。但是有一个问题就是"ab"最终为什么常量池中没有呢。我们可以想一下前面所讲的, 哪种String构造方法只产生堆中对象,不产生常量池中的呢?就是我们前面讲的传入char的String构造方法啦~ 那么我们可以猜测,是不是StringBuilder.toString方法里面构造String时是传入的char。那么这个"ab "的值在StringBuilder中也一定是用char存储的啦,再通过toString传入char构造String。

  • 下面我们看一下StringBuilder的部分源码~~
    // StringBuilder 重写的toString方法
    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }
   // 点入这个value
     /**
     * The value is used for character storage.
     */
    char[] value;

通过这段源码,我们可以看到,toString时是把StringBuilder中的 char value 传入String构造函数,所以并没有在常量池中生成"ab",只存在于常量池。而产生的"a" "b" 会因为缺少root引用而被很快的回收的~~。


五 . 问题答案讲解

        String var1 = new String("a");
        char ch[] = {'a'};
        String var2 = new String(ch);

        System.out.println(var1 +"++"+ var2); // a++a
        System.out.println(var1 == var2); // false
        System.out.println(var1 == var2.intern()); // false
        System.out.println(var1.intern() == var2); // false
        System.out.println(var1.intern() == var2.intern()); // true

var1 在堆中,常量池中创建对象。var2只在堆中创建对象(并且value对象地址与var1的不同)。特别要注意的是当var2.intern()时,常量池中已经有了"a"所以不会再插一个引用进去常量池,intern返回的就是堆中"a"的地址与var1.intern一样~~。


  • 下面通过反射修改值也就是对上述的验证啦~~
        String var1 = new String("a"); 
        char ch[] = {'a'};
        String var2 = new String(ch);
        var2.intern();
        System.out.println(var1 +"++"+ var2);// a++a
        // 这时我们开始修改var2的值啦~~~
        // 反射修改String 的值,看是否是共用关系
        Field field = String.class.getDeclaredField("value");
        field.setAccessible(true); // 因为 char 为 private,所以设置 char value 为可以访问,修改
        char[] char1 = (char[]) field.get(var2);
        char1[0] = 'b';// 修改var2的值为b
        System.out.println(var1+"+++"+var2);// a++b

这里我们可以发现,var2的value只存在与堆中,修改他的值不会影响var1的哦~但是把构建顺序换一下,就不一样啦~~~

        char ch[] = {'a'};
        String var2 = new String(ch);
        var2.intern();
        String var1 = new String("a");
        System.out.println(var1 +"++"+ var2);// a++a
        // 这时我们开始修改var2的值啦~~~
        // 反射修改String 的值,看是否是共用关系
        Field field = String.class.getDeclaredField("value");
        field.setAccessible(true); // 因为 char 为 private,所以设置 char value 为可以访问,修改
        char[] char1 = (char[]) field.get(var2);
        char1[0] = 'b';// 修改var2的值为b
        System.out.println(var1+"+++"+var2);// b++b

调换了顺序,先加载了var2,这时只在堆中存在对象,然后再调用intern将var2的引用加入常量池。当构建var1 时在常量池中会将var2的引用加载的到var1中。这时var1 var2的value时同一个char,修改其中一个另一个也会改变~


        char ch[] = {'a'};
        String var2 = new String(ch);
        var2.intern();
        String var1 = "a";
        System.out.println(var1 == var2); // true

这种也是一样的结果啦,var2只在堆中创建对象,没有在常量池中创建,调用intern方法会将var2的引用加入常量池中。这时var1其实就是指向堆中的var2。所以var1 var2当然是相等的啦。


        String  var1 = new String("a") + new String("b");
        String  var2 = new String("a") + new String("b");
        // 这里肯定是ture啦~
        System.out.println(var1.intern() == var2.intern() ); // true

        String var3 = "ab";
        // 但是下面这几个呢?
        System.out.println(var3 == var1); // true
        System.out.println(var3 == var2); // false
        System.out.println(var3 == var2.intern());// true

        // 这样又是什么结果呢?
        System.out.println(var1 == var2.intern());// true
        System.out.println(var1.intern() == var2);// false

这个应该算是比较绕的了,但如果看懂了前面的所有String构建方式,再逐行分析。 首先var1 var2 会在堆中生成两个"ab" "ab" 对象(不是一个哦)。

var1.intern() == var2.intern() 先调用的是var1.intern那么常量池中的引用就是var1对象的引用。这时var2.intern其实找到的也是var1的引用,不再插入var2的啦。

var3 == var1 之后定义的"ab"是指向var1的引用的,所以是true没有问题。

var3 == var2 因为var3的引用是var1的,所以与var2不相等。

var3 == var2.intern() 当var2.intern时返回的是var1的引用,这时就跟var3相同啦

var1 == var2.intern() var2.intern 返回的就是常量池中var1的引用所欲相等。

var1.intern() == var2 这时var1.intern返回的就是自己的引用自然与var2不相等啦~~~


六 . 为什么JDK1.8 要将常量池移入堆中?

通过这个常量池中插入的是引用,其实可以想到,这样可以节约空间并且更容易被回收。原先放在永久代中的常量池中的对象很难被回收,就会造成大量的空间浪费。