好久没做java了,最近突然想复习复习java的东西,就想起来以前搞的最多的一个问题,也是面试中常被问到的问题,就是String,StringBuffer,
StringBuilder的区别。这个也是网上很多人都写过了,我就是总结了下,希望大家共勉。
首先,介绍这个东西之前,我还是希望大家掌握一些必须要知道的概念:堆,堆栈和常量池。
在JAVA中,有六个不同的地方可以存储数据:
1. 寄存器(register)。这是最快的存储区,因为它位于不同于其他存储区的地方——处理器内部。但是寄存器的数量极其有限,所以寄存器由编译器根据需求进行分配。你不能直接控制,也不能在程序中感觉到寄存器存在的任何迹象。
------最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制.
2. 堆栈(stack)。位于通用RAM中,但通过它的“堆栈指针”可以从处理器哪里获得支持。堆栈指针若向下移动,则分配新的内存;若向上移动,则释放那些 内存。这是一种快速有效的分配存储方法,仅次于寄存器。创建程序时候,JAVA编译器必须知道存储在堆栈内所有数据的确切大小和生命周期,因为它必须生成 相应的代码,以便上下移动堆栈指针。这一约束限制了程序的灵活性,所以虽然某些JAVA数据存储在堆栈中——特别是对象引用,但是JAVA对象不存储其 中。
------存放基本类型的变量数据和对象,数组的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(字符串常量对象存放在常量池中)
3. 堆(heap)。一种通用性的内存池(也存在于RAM中),用于存放所以的JAVA对象。堆不同于堆栈的好处是:编译器不需要知道要从堆里分配多少存储区 域,也不必知道存储的数据在堆里存活多长时间。因此,在堆里分配存储有很大的灵活性。当你需要创建一个对象的时候,只需要new写一行简单的代码,当执行 这行代码时,会自动在堆里进行存储分配。当然,为这种灵活性必须要付出相应的代码。用堆进行存储分配比用堆栈进行存储存储需要更多的时间。
------存放所有new出来的对象。
4. 静态存储(static storage)。这里的“静态”是指“在固定的位置”。静态存储里存放程序运行时一直存在的数据。你可用关键字static来标识一个对象的特定元素是静态的,但JAVA对象本身从来不会存放在静态存储空间里。
------存放静态成员(static定义的)
5. 常量存储(constant storage)。常量值通常直接存放在程序代码内部,这样做是安全的,因为它们永远不会被改变。有时,在嵌入式系统中,常量本身会和其他部分分割离开,所以在这种情况下,可以选择将其放在ROM中
------存放字符串常量和基本类型常量(public static final)
6. 非RAM存储。如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。
------硬盘等永久存储空间
就速度来说,有如下关系:
寄存器 > 堆栈 > 堆 > 其他
这里我们主要关心栈,堆和常量池,对于栈和常量池中的对象可以共享,对于堆中的对象不可以共享。栈中的数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会消失。堆中的对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定,具有很大的灵活性。
对于字符串:其对象的引用都是存储在栈中的,如果是编译期已经创建好(直接用双引号定义的)的就存储在常量池中,如果是运行期(new出来的)才能确定的就存储在堆中。对于equals相等的字符串,在常量池中永远只有一份,在堆中有多份。
如以下代码:
1. String s1 = "china";
2. String s2 = "china";
3. String s3 = "china";
4. String ss1 = new String("china");
5. String ss2 = new String("china");
6. String ss3 = new String("china");
对于通过new产生一个字符串(假设为”china”)时,会先去常量池中查找是否已经有了”china”对象,如果没有则在常量池中创建一个此字符串对象,然后堆中再创建一个常量池中此”china”对象的拷贝对象。这也就是有道面试题:String s = new String(“xyz”);产生几个对象?一个或两个,如果常量池中原来没有”xyz”,就是两个。
对于基础类型的变量和常量:变量和引用存储在栈中,常量存储在常量池中。
如以下代码:
1. int i1 = 9;
2. int i2 = 9;
3. int i3 = 9;
4. public staticfinalint INT1 =9;
5. public staticfinalint INT2 =9;
6. public staticfinalint INT3 =9;
对于成员变量和局部变量:成员变量就是方法外部,类的内部定义的变量;局部变量就是方法或语句块内部定义的变量。局部变量必须初始化。
形式参数是局部变量,局部变量的数据存在于栈内存中。栈内存中的局部变量随着方法的消失而消失。
成员变量存储在堆中的对象里面,由垃圾回收器负责回收。
如以下代码:
1. class BirthDate {
2. private int day;
3. private int month;
4. private int year;
5. public BirthDate(int d,int m,int y) {
6. day = d;
7. month = m;
8. year = y;
9. }
10. 省略get,set方法………
11. }
12.
13. public class Test{
14. public staticvoid main(String args[]){
15. int date = 9;
16. Test test = new Test();
17. test.change(date);
18. BirthDate d1= new BirthDate(7,7,1970);
19. }
20.
21. public void change1(int i){
22. i = 1234;
23. }
1. class BirthDate {
2. private int day;
3. private int month;
4. private int year;
5. public BirthDate(int d,int m,int y) {
6. day = d;
7. month = m;
8. year = y;
9. }
10. 省略get,set方法………
11. }
12.
13. public class Test{
14. public staticvoid main(String args[]){
15. int date = 9;
16. Test test = new Test();
17. test.change(date);
18. BirthDate d1= new BirthDate(7,7,1970);
19. }
20.
21. public void change1(int i){
22. i = 1234;
23. }
对于以上这段代码,date为局部变量,i,d,m,y都是形参为局部变量,day,month,year为成员变量。下面分析一下代码执行时候的变化:
1. main方法开始执行:int date = 9;
date局部变量,基础类型,引用和值都存在栈中。
2. Test test = new Test();
test为对象引用,存在栈中,对象(newTest())存在堆中。
3. test.change(date);
i为局部变量,引用和值存在栈中。当方法change执行完成后,i就会从栈中消失。
4. BirthDate d1= new BirthDate(7,7,1970);
d1为对象引用,存在栈中,对象(newBirthDate())存在堆中,其中d,m,y为局部变量存储在栈中,且它们的类型为基础类型,因此它们的数据也存储在栈中。day,month,year为成员变量,它们存储在堆中(new BirthDate()里面)。当BirthDate构造方法执行完之后,d,m,y将从栈中消失。
5.main方法执行完之后,date变量,test,d1引用将从栈中消失,new Test(),new BirthDate()将等待垃圾回收。
好了,有了上面的基础,下面我们就来解释String,StringBuffer,StringBuilder的区别了。
一、概貌
String:
是对象不是原始类型.
为不可变对象,一旦被创建,就不能修改它的值.
对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去.
String 是final类,即不能被继承.
StringBuffer:
是一个可变对象,当对他进行修改的时候不会像String那样重新建立对象
它只能通过构造函数来建立,
StringBuffer sb = new StringBuffer();
note:不能通过付值符号对他进行付值.
sb = "welcome to here!";//error
对象被建立以后,在内存中就会分配内存空间,并初始保存一个null.向StringBuffer
中付值的时候可以通过它的append方法.
StringBuilder:
此类从jdk1.5开始,它是一个可变字符序列,这点跟StringBuffer是一样的。它比StringBuffer出现的晚,一般我们认为它是StringBuffer的替代品。StringBuffer同步,它不同步,因此也就意味着在单处理机中它的速度要比StringBuffer快。因为同步是耗资源的。
二、详细阐述
怎么理解概述中的String类的对象不可改变呢?
1.String a = "123";
a = "456";
下面详细解释下这个创建过程:第一句,会在常量池中创建一个常量串”123”,并且在栈中开辟一块引用a,使这个引用指向常量池中的“123“这个常量串的地址。第二句话产生的效果是把a这个引用重新指向了一个新的常量串”456“这个地址。而原来的常量串”123“仍然存在常量池中,等待系统的回收。
2.String a=new String(“123”);
这种创建个效果跟上面有些不同。相同点都是会在常量池中开辟一块常量空间存放常量串“123“。不同的是上面是在栈中创建一个引用a,指向常量”123“。而这个是创建两个对象。一个常量对象,也就是常量池中的常量串,另一个New String()也会在堆中创建一个对象,此对象中内容是常量池中”123“常量串的地址。(注意:这点很重要!)。new String()创建的堆对象里面并不是直接就存的是常量串”123“的拷贝值。
3.StringBuffer sb = newStringBuffer("asd");
sb.append("fgh");
在这个过程中,只存在sb这么一个对象,sb一直都指向一个 StringBuffer instance。*.append也只是改变此 instance的内容而已。
String是一个final Class, StringBuffer不是。所以对于 String a = "yacht";String b = "yacht1"; String c = a + b ; 存在一个对象拷贝构造和解析的消耗问题;
解释下这段代码:在java中,String c=a + b;实际上是如下执行的:
StringBuffer c=new StringBuffer();
c.append(a);
c.append(b);
c.toString();
看到了吧,对String对象进行连接字符串的操作,在jvm内部,还是通过StringBuffer的机制实现的。
对于一个StringBuffer来说,StringBuffer sb = newStringBuffer();sb.append("yacht"); sb.append("yacht1"); 因为StringBuffer是一个可以实例化的类,而且它的内建机制是维护了一个capacity大小的字符数组,所以它的append操作不存在对象的消耗问题,所以我觉得如果存在String连接这种事情,StringBuffer来做会好很多。
但事情并不是这么简单,看下面代码:
String a = "yacht1" +"yacht2" + "yacht3" + "yacht4";
StringBuffer sb = new StringBuffer();
sb.append("yacht1") ;
sb.append("yacht2");
sb.append("yacht3") ;
sb.append("yacht4");
String a = sb.toString();
如果按照我上面说的,String的效率肯定比StringBuffer的低,但经过测试不是这样,为什么?这里,我们需要理解程序过程的两个时期,一个是编译时,一个是运行时,在编译时,编译器会对你的程序做出优化,所以String a会被优化成yacht1yacht2yacht3yacht4
如果代码是这样的:
String a ;
for(int i = 0; i< 100000;i++){
a += String.valueOf(i) ;
}
StringBuffer sb = new StringBuffer();
for(int i = 0; i< 100000;i++){
sb.append(i) ;
}
String a = sb.toString();
如果是这种情况的话,String的效率就大大不如StringBuffer,区别在哪里,就在于运行时和编译时的优化问题上!
public class xxx {
public static void main(String[] args) {
String s1 = "You are hired!";
String s2 = "You are hired!";
if (s1==s2) {
System.out.println("一个内存空间");
}
else {
System.out.println("不是一个内存空间");
}
}
}
打印的结果是:一个内存空间。这里==的意义是两个操作数是否指向同一个内存空间。可见s2在不用new创建的情况下会自动检索到具有相同内容的常量池中空间并把s2也指向这个内存空间,所以当然就是true了。
在看下面的代码:
public class xx {
public static void main(String[] args) {
String s1 = "You are hired!";
String s2 = "You are hired!";
s1 = s1.replace('h','f');
System.out.println(s1);
if (s1==s2) {
System.out.println("一个内存空间");
} else {
System.out.println("不是一个内存空间");
}
}
}
代码结果是You are fired!不是一个内存空间。可见,String中s1的内容虽然被改写,但是已经不在是原来第一次分配到的那个内存空间了。也就是String类的内容能被改变,但一旦改变系统将为其分配新的内存空间。说到与stringBuffer的区别,从根本上来说应该是stringBuffer在做字符长度变动的时候将继续使用原来的内存空间,不新分配.而String的长度一旦变动,就如上面的例子一样,其内部将分配新的内存空间。
StringBuffer必须new出来,StringBuffer的append的效率比string的+=的效率高,仔细研究发现还有很大的区别,看看以前scjp的考题
public class Test {
public static void stringReplace (Stringtext) {
text = text.replace('j' , 'i');
}
public static void bufferReplace(StringBuffer text) {
text = text.append("C");
}
public static void main (String args[]) {
String textString = new String ("java");
StringBuffer textBuffer = new StringBuffer("java");
stringReplace (textString);
bufferReplace (textBuffer);
System.out.println (textString +textBuffer);
}
}
这段代码打印什么呢?是 javajavaC还是iavajavaC呢?哈哈,我这么问了,大家当然知道是打印是 javajavaC了。这是为什么呢?原文章中说的中心就是一句话:String的值是不会变的!首先肯定这个回答是正确的,但是不是很明了。为什么呢?看我逐个分析:
stringReplace (textString);
bufferReplace (textBuffer);
这是上面的代码。是两个函数调用。而且函数调用的参数传递的是对象。好吧,大家都明白,对象传递,传递的是对象的地址,那么这两句话是把前面声明的String类和StringBuffer类的在堆内存中开辟的地址分别传递给了这两个函数。此时,当StringBuffer的对象到函数中去了,就更改了这个地址里面原有的内容了,我们看到,这调用append方法在原有内容后面加上了个‘C’。这个都没问题。关键就是String,它命名传递过去的就是String对象在堆内存中的地址,按道理说在方法中改变了地址中的值后,返回的就是改变后的值了嘛!这里怎么值还是没变呢?这里大家注意:String在堆内存中存的不是直接的“java“这个值,而是常量池中的”java“这个串的引用(地址)。当我们要去改变String这个对象的值时,jvm一看,是String中存的是地址,它就去常量池中找这个地址,找到了这个地址,要改变它,jvm就重新开辟一块空间,存储改变后的地址。这个改变后的地址是由调用函数中的形参引用的,在本例中,是
public static void stringReplace (Stringtext) {
text = text.replace('j' , 'i');
}
函数中的text引用的。但是原来的textString还是指向的常量池中的“java“串的地址。这就是: String的值是不会变的!这句话在内存中是怎么实现的解释。相信现在大家就该记住了,我们说的对象传递,传递的是地址,这句话没错,可是到了String面前,它就不灵了(绝对不是错了!),String的对象仍然是传地址,但是它的效果是等同于传值的。希望大家记住。
以下做为扩展:
StringBuffer s1 = newStringBuffer("a");
StringBuffer s2 = newStringBuffer("a");
s1.equals(s2)//为什么是false
String s1 = new String("a");
String s2 = new String("a");
s1.equals(s2)//为什么是true
StringBuffer类中没有重新定义equals这个方法,因此这个方法就来自Object类,而Object类中的equals方法是用来比较地址的,所以等于false.String类中重新定义了equals这个方法,而且比较的是值,而不是地址。所以会是true。
三、StringBuilder
其用法类似于StringBuffer,这里大家只需要知道它是线程不安全的(即不同步)。它在单处理机中可以安全的替代StringBuffer就可以了。其用法和方法详细请参见jdk1.6帮助文档。