上一篇在介绍java内存结构时提到了方法区这块,本地方法区存在一块特殊的内存区域,叫常量池(Constant Pool),这块内存将与String类型的分析密切相关。
1.String的本质
首先看下面一段代码:
String str1 = "hadoop";
String str2 = "android";
str1 = str1+str2;
System.out.println(str1)
输出结果可想而知:hadoopandroid.
我们都知道String是值不可变(immutable)的常量,是线程安全的(can be shared)。为什么此时会输出hadoopandroid了?首先在栈中有个str1变量指向堆中的"hadoop"对象,栈中"str2"变量指向堆中的"android"对象,当执行到str1 = str1 + str2时,系统重新在堆中new一个更大的数组出来,然后将"hadoop"和"android"都复制进去,然后栈中的str1指向这个新new出来的数组。所谓的不可变是指:它没有在组"hadoop"上进行修改,而是新建了个更大数组进行扩展,也就是说,这时候堆里还是有"hadoop"这个对象数组存在的,只不过这个时候str1不再指向这个"hadoop"数组了,而是指向了另外一个新建的数组。
打开String的源码,类注释中有这么一段话“Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings.Because String objects are immutable they can be shared.”。这句话总结归纳了String的一个最重要的特点:String是值不可变(immutable)的常量,是线程安全的(can be shared)。
接下来,String类使用了final修饰符,表明了String类的第二个特点:String类是不可继承的。
下面是String类的成员变量定义,从类的实现上阐明了String值是不可变的(immutable)。
private final char value[];
private final int count;
因此,我们看String类的concat方法。实现该方法第一步要做的肯定是扩大成员变量value的容量,扩容的方法重新定义一个大容量的字符数组buf。第二步就是把原来value中的字符copy到buf中来,再把需要concat的字符串值也copy到buf中来,这样子,buf中就包含了concat之后的字符串值。下面就是问题的关键了,如果value不是final的,直接让value指向buf,然后返回this,则大功告成,没有必要返回一个新的String对象。但是。。。可惜。。。由于value是final型的,所以无法指向新定义的大容量数组buf,那怎么办呢?“return new String(0, count + otherLen, buf);”,这是String类concat实现方法的最后一条语句,重新new一个String对象返回。这下真相大白了吧!
总结:String实质是字符数组,两个特点:1、该类不可被继承;2、不可变性(immutable)。
2.String常见的定义方法及误区
1)
String a = "abc"; //.1
String b = "abc"; //.2
System.out.println(a==b);
输出结果为true。
分析:
1代码执行后在常量池(constant pool)中创建了一个值为abc的String对象,2执行时,因为常量池中存在"abc"所以就不再创建新的String对象了栈中两个引用地址同时指向"abc"。
2)
String c = new String("xyz");①
String d = new String("xyz");②<span style="font-family: Arial, Helvetica, sans-serif;"> </span>
<pre name="code" class="java" style="color: rgb(51, 51, 51); letter-spacing: 0.5px; line-height: 22.5px;"> System.out.println(c==d);
分析:①Class被加载时,"xyz"被作为常量读入,在常量池(constant pool)里创建了一个共享的值为"xyz"的String对象;然后当调用到new String("xyz")的时候,会在堆(heap)里创建这个new String("xyz")对象;②由于常量池(constant pool)中存在"xyz"所以不再创建"xyz",然后创建新的new String("xyz")。在堆中创建了两个对象,栈中引用对象地址不一样,故输出false.
3)
String s1 = new String("xyz"); //创建二个对象(常量池和堆中),一个引用
String s2 = new String("xyz"); //创建一个对象(堆中),并且以后每执行一次创建一个对象,一个引用
String s3 = "xyz"; //创建一个对象(常量池中),一个引用
String s4 = "xyz"; //不创建对象(共享上次常量池中的数据),只是创建一个新的引用
System.out.println(s1==s3) //false
这里在进行详细分析。第一种方式通过关键字new定义过程:在程序编译期,编译程序先去字符串常量池检查,是否存在“xyz”,如果不存在,则在常量池中开辟一个内存空间存放“xyz”;如果存在的话,则不用重新开辟空间,保证常量池中只有一个“xyz”常量,节省内存空间。然后在内存堆中开辟一块空间存放new出来的String实例,在栈中开辟一块空间,命名为“s1”,存放的值为堆中String实例的内存地址,这个过程就是将引用s1指向new出来的String实例。各位,最模糊的地方到了!堆中new出来的实例和常量池中的“xyz”是什么关系呢?看下图分析:
常量池中的xyz实际上是通过其地址把String.value的值赋给堆中的实例。从String类的public String(String original) {
int size = original.count;
char[] originalValue = original.value;这两行代码也可以看出来,其实就相当于一份拷贝而已,然后栈中引用s1指向堆中的实例,当接着执行String s3 = "xyz";时,因为池中已经存在“xyz”的实例对象,则s3直接指向池中的实例对象;否则,在池中先创建一个实例对象,s3再指向它。
3、String、StringBuffer、StringBuilder的联系与区别
上面已经分析了String的本质了,下面简单说说StringBuffer和StringBuilder。
StringBuffer和StringBuilder都继承了抽象类AbstractStringBuilder,这个抽象类和String一样也定义了char[] value和int count,但是与String类不同的是,它们没有final修饰符。因此得出结论:String、StringBuffer和StringBuilder在本质上都是字符数组,不同的是,在进行连接操作时,String每次返回一个新的String实例,而StringBuffer和StringBuilder的append方法直接返回this,所以这就是为什么在进行大量字符串连接运算时,不推荐使用String,而推荐StringBuffer和StringBuilder。那么,哪种情况使用StringBuffe?哪种情况使用StringBuilder呢?
关于StringBuffer和StringBuilder的区别,翻开它们的源码,下面贴出append()方法的实现。
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
public StringBuilder append(String str) {
super.append(str);
return this;
}
上面第一张图是StringBuffer中append()方法的实现,第二张图为StringBuilder对append()的实现。区别应该一目了然,StringBuffer在方法前加了一个synchronized修饰,起到同步的作用,可以在多线程环境使用。为此付出的代价就是降低了执行效率。
因此,如果在多线程环境可以使用StringBuffer进行字符串连接操作,单线程环境使用StringBuilder,它的效率更高。
以上只是个人理解,如有错误忘指出。