从为什么String=String谈到StringBuilder和StringBuffer

前言

有这么一段代码:

复制代码
1 public class TestMain
2 {
3     public static void main(String[] args)
4     {
5         String str0 = "123";
6         String str1 = "123";
7         System.out.println(str0 == str1);
8     }
9 }
复制代码

运行结果是什么?答案当然是true。对,答案的确是true,但是这是为什么呢?很多人第一反应肯定是两个"123"的String当然相等啊,这还要想。但是"=="在Java比较的根本不是两个对象的值,而是比较两个对象的引用是否相等,和两个String都是"123"又有什么关系呢?或者我们把程序修改一下

复制代码
1 public class TestMain
2 {
3     public static void main(String[] args)
4     {
5         String str2 = new String("234");
6         String str3 = new String("234");
7         System.out.println(str2 == str3);
8     }
9 }
复制代码

这时候运行结果就是false了,因为尽管两个String对象都是"234",但是str2和str3是两个不同的引用,所以返回的false。OK,围绕第一段代码返回true,第二段代码返回false,开始文章的内容。

为什么String=String?

在JVM中有一块区域叫做常量池,关于常量池,我在写虚拟机的时候有专门提到http://www.cnblogs.com/xrq730/p/4827590.html。常量池中的数据是那些在编译期间被确定,并被保存在已编译的.class文件中的一些数据。除了包含所有的8种基本数据类型(char、byte、short、int、long、float、double、boolean)外,还有String及其数组的常量值,另外还有一些以文本形式出现的符号引用。

Java栈的特点是存取速度快(比堆块),但是空间小,数据生命周期固定,只能生存到方法结束。我们定义的boolean b = true、char c = 'c'、String str = “123”,这些语句,我们拆分为几部分来看:

1、true、c、123,这些等号右边的指的是编译期间可以被确定的内容,都被维护在常量池中

2、b、c、str这些等号左边第一个出现的指的是一个引用,引用的内容是等号右边数据在常量池中的地址

3、boolean、char、String这些是引用的类型

栈有一个特点,就是数据共享。回到我们第一个例子,第五行String str0 = "123",编译的时候,在常量池中创建了一个常量"123",然后走第六行String str1 = "123",先去常量池中找有没有这个"123",发现有,str1也指向常量池中的"123",所以第七行的str0 == str1返回的是true,因为str0和str1指向的都是常量池中的"123"这个字符串的地址。当然如果String str1 = "234",就又不一样了,因为常量池中没有"234",所以会在常量池中创建一个"234",然后str1代表的是这个"234"的地址。分析了String,其实其他基本数据类型也都是一样的:先看常量池中有没有要创建的数据,有就返回数据的地址,没有就创建一个

第二个例子呢?Java虚拟机的解释器每遇到一个new关键字,都会在堆内存中开辟一块内存来存放一个String对象,所以str2、str3指向的堆内存中虽然存储的是相等的"234",但是由于是两块不同的堆内存,因此str2 == str3返回的仍然是false,网上找到一张图表示一下这个概念:

从为什么String=String谈到StringBuilder和StringBuffer_第1张图片

为什么要使用StringBuilder和StringBuffer拼接字符串?

大家在开发中一定有一个原则是"利用StringBuilder和StringBuffer拼接字符串",但是为什么呢?用一段代码来分析一下:

复制代码
 1 public class TestMain
 2 {
 3     public static void main(String[] args)
 4     {
 5         String str = "111";
 6         str += "222";
 7         str += "111";
 8         str += "444";
 9         System.out.println(str);
10     }
11 }
复制代码

分析一下代码运行过程:

1、第5行,去常量池中找"111",没找到,常量池中创建一个,str指向常量池中的“111”

2、第6行,去常量池中找拼接后的"111222",没找到,常量池中创建一个,str指向常量池中的“111222”

3、第7行,去常量池中找拼接后的"111222111",没找到,常量池中创建一个,str指向常量池中的“111222111”

4、第8行,去常量池中找拼接后的"111222111444",没找到,常量池中创建一个,str指向常量池中的“111222111444”

看到了吧,这就是String拼接字符串的坏处。我们最终只需要"111222111444",但是却给我们在常量池中创建了这么多中间常量"111"、"111222"、"111222111"。这意味着,用String拼接字符串可能会导致常量池中产生大量的无用的常量(万一创建的常量池中本身就有呢),消耗内存空间,虽然一两个String不算什么,但是积少成多,代码中各个地方都使用String拼接字符串,那就是极大的消耗

同时,这就是要使用StringBuilder和StringBuffer的原因,以StringBuilder为例:

复制代码
 1 public class TestMain
 2 {
 3     public static void main(String[] args)
 4     {
 5         StringBuilder sb = new StringBuilder("111");
 6         sb.append("222");
 7         sb.append("111");
 8         sb.append("111");
 9         sb.append("444");
10         System.out.println(sb.toString());
11     }
12 }
复制代码

StringBuffer和StringBuilder原理一样,无非是在底层维护了一个char数组,每次append的时候就往char数组里面放字符而已,在最终sb.toString()的时候,用一个new String()方法把char数组里面的内容都转成String,这样,常量池中创建了一个最终产生的String,在需要对字符串进行拼接尤其是大量拼接的地方,大量地节省常量池的空间。

StringBuffer和StringBuilder用法一模一样,唯一的区别只是StringBuffer是线程安全的,它对所有方法都做了同步,StringBuilder是线程非安全的,所以在不涉及线程安全的场景,比如方法内部,尽量使用StringBuilder,避免同步带来的消耗。另外,StringBuffer和StringBuilder还有一个优化点,如果可以估计到要拼接的字符串的长度的话,尽量利用构造函数指定他们的长度,避免数组扩容带来的消耗,这个之后写List的时候会专门写到。

小心陷阱

虽然说不要用"+"拼接字符串,因为会产生大量的无用常量,但也不是不可以,比如可以使用以下的方式:

复制代码
1 public class TestMain
2 {
3     public static void main(String[] args)
4     {
5         String str = "111" + "222" + "333" + "444";
6         System.out.println(str);
7     }
8 }
复制代码

这么做,实际上编译的时候,Java会把"111"、"222"、"333"、"444"都拼接直接拼接在一起当作一整个字符串来看,然后给str,实际上这样,在常量池中只有一个"111222333444",并不会产生无用的常量。不过这么写得很少,主要原因有两点:

1、例子比较简单,但实际上大量的“+”会导致代码的可读性非常差

2、待拼接的内容可能从各种地方获取,比如调用接口、从.properties文件中、从.xml文件中,这样的场景下尽管用多个“+”的方式也不是不可以,但会造成不方便

你可能感兴趣的:(java)