前言
有这么一段代码:
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,网上找到一张图表示一下这个概念:
为什么要使用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文件中,这样的场景下尽管用多个“+”的方式也不是不可以,但会造成不方便