java.lang.String 的不可变性

1. 前言

   需要创建一个String类的对象时,可以选择直接

String str1 = "abc";

  或者选择 

String str2 = new String("abc");

  第一种方式称之为 字面量定义方式,其赋值方式和基本数据类型的赋值方式是一样的;所以会有很多初学小伙伴误以为 String 是基本数据类型,其实不然。第二种方式就是一般的构造器创建对象的方式。

 

2. String 的底层结构

   查看String类的源码

  

  可以发现String其实是一个char类型的数组构成的,并且其用到了一个很重要的关键字 final。既然使用了 final ,那么 value[] 在被赋值之后将不再会被改变。到这里可能有小伙伴就会有疑问了:String类的对象被赋值之后仍然是可以改变的,并不符合 final 关键字的作用,这是为什么呢?String类的不可变性又体现在哪里?

 

3. String 重要特性--不可变性

   先来看一段简单的测试代码:

 1    @Test
 2     public void test1() {
 3         String str1 = "abc"; 
 4         String str2 = "abc";
 5         System.out.println(str1 == str2);  //true
 6 
 7         str1 = "123";
 8         System.out.println(str1 == str2);  //false
 9 
10         String str3 = "abc";
11         str3 += "def";
12         System.out.println(str3 == str2);  //false
13     }

  我们都知道”==“在比较对象时比较的是对象的地址。通过测试发现 str1 和 str2 的地址是一样的,当把 str1 的值改为 "123" 之后,str1 与 str2 的地址又不一样了。代码中并没有动过 str2 ,不难想到肯定是 str1 指向了另外的地址。那么 str1 为什么指向了别的地址,而不是在原来指向的内存区域里面做修改呢?当然是因为 final 关键字啦。str1 被改为 "123" 之后,并不能在原来的内存区域修改,而是需要重新开辟一个新的内存区域来存放 "123",所以第二个结果自然就是 false,同理可推测第三个结果也是 false。

  到这里对 String 的不可变性是否有一点认识了呢?还有一个问题,那就是 str1 == str2,为什么是 true 呢?

  其实在JVM内存划分中有个方法区,方法区中有个字符串常量池,我们需要知道的是String类的对象存储的字符串都会放在字符串常量池中,并且字符串常量池中不会存储两个一样的字符串。具体通过一个示意图来看看JVM在底层到底是怎么做的吧:

  

  以字面量定义方式每创建一个String对象时,若该字符串在字符串常量池中已经存在,则将其地址返回给新创建的对象;若不存在则在字符串常量池开辟一个区域存放这个字符串,再将这个地址返回给新创建的对象。修改时也是如此,修改后的字符串在字符串常量池中存在则将其地址返回,不存在则新开辟内存区域存放修改后的字符串。

  刚开始,str1 创建时需要在字符串常量池中开辟区域存放 "abc",并将其地址返回给 str1;之后 str2 和 str3 创建时,"abc" 在字符串常量池中已经存在了,所以直接获取到其地址。str1、str2 和 str3 都指向存放 "abc" 的地址,它们的值都是 "abc"。把 str1 修改为 "123" 时,JVM会在字符串常量池中开辟一个新的区域存放 "123" ,并让 str1 指向这个新的的地址(即0x1001),所以此时再比较 str1 == str2 得到的结果肯定就是 false 了。试想若不开辟新的内存区域,直接在原来地址0x1000区域存放的数据改了,那么 str2 和 str3 也会被改;这样肯定是不对的,修改 str1 不能影响 str2 和 str3 。以后若要是有String类的对象 str4、str5、......的值为 "123" ,那么它们得到的地址仍然是存储 "123" 的地址 0x1001。

  以上是通过字面量定义的方式,我们再看一个例子:

    @Test
    public void test2(){
        String s1 = "abc";
        String s2 = "abc";

        String s3 = "abc";
        s3 += "def";

        String s4 = new String("abc");
        String s5 = new String("abc");

        System.out.println(s1 == s2);   //true
        System.out.println(s2 == s4);   //false
        System.out.println(s4 == s5);   //false
    }

   我们知道通过new创建出来的对象是放在堆空间的,在这里 s4 和 s5 是通过new方式得到的,是放在堆空间的两个不同对象,其地址自然是不一样的。但它们两个的内容又都是 "abc",它们和字符串常量池中的 "abc" 又是什么联系呢?

   

  通过上述示意图,我们发现 s4 和 s5 拥有不同的地址,但它们都指向了"abc",换句话说它们存放的都是位于字符串常量池中 "abc" 的地址。此时比较 s2 == s4,就是拿地址 0x7799(内容为0x1122) 和地址 0x1122(内容为 "abc") 作比较,结果自然是 false。

  以上就是String类不可变性的具体体现,其代表不可变的字符序列。

 

4. StringBuffer、StringBuilder

   String 代表不可变的字符序列,有不可变的自然就有可变的。StringBuilder 和 StringBudder 就代表可变的字符序列,修改它们的对象的内容时,确实是在其所指向的内存区域做了修改。

  细心的小伙伴会发现String类对字符串进行增、删、改的方法返回的对象和原来对象作 == 比较总是false,而 StringBuilder 和 StringBudder 总是 true 。原因就在于可变与不可变。

 

5. 两个面试题

1. String str = new String("aaa");方式创建String对象,在内存中创建了几个对象?
答:两个。一个是new出来存放在堆空间的str对象,另一个是char[]对应字符串常量池中的 aaa。
2.
public class Test {
    String str = new String("good");
    char[] ch = {'t','e','s','t'};
 
    public void change(String str, char[] ch){
        str ="test ok";
        ch[0] = 'b';
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.change(test.str, test.ch);

        System.out.println(test.str);  //good
        System.out.println(test.ch);   //best
    }
}

 

猜你喜欢

转载自www.cnblogs.com/wpf22/p/11412814.html