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 } }