String、StringBuffer与StringBuilder之间区别
关于这三个类在字符串处理中的位置不言而喻,那么他们到底有什么优缺点,到底什么时候该用谁呢?下面我们从以下几点说明一下
StringBuffer、StringBuilder和String一样,也用来代表字符串。String类是不可变类,任何对String的改变都 会引发新的String对象的生成; StringBuffer则是可变类,任何对它所指代的字符串的改变都不会产生新的对象。既然可变和不可变都有了,为何还有一个StringBuilder呢?相信初期的你,在进行append时,一般都会选择StringBuffer吧!
先说一下集合的故事,HashTable是线程安全的,很多方法都是synchronized方法,而HashMap不是线程安全的,但其在单线程程序中的性能比HashTable要高。StringBuffer和StringBuilder类的区别也是如此,他们的原理和操作基本相同,区别在于StringBufferd支持并发操作,线性安全的,适 合多线程中使用。StringBuilder不支持并发操作,线性不安全的,不适合多线程中使用。新引入的StringBuilder类不是线程安全的,但其在单线程中的性能比StringBuffer高。
1.三者在执行速度方面的比较:StringBuilder > StringBuffer > String
2.String <(StringBuffer,StringBuilder)的原因
String:字符串常量
StringBuffer:字符创变量
StringBuilder:字符创变量
从上面的名字可以看到,String是“字符创常量”,也就是不可改变的对象。对于这句话的理解你可能会产生这样一个疑问 ,比如这段代码:
2 s = s + 1 ;
3 System.out.print(s); // result : abcd1
我们明明就是改变了String型的变量s的,为什么说是没有改变呢? 其实这是一种欺骗,JVM是这样解析这段代码的:首先创建对象s,赋予一个abcd,然后再创建一个新的对象s用来 执行第二行代码,也就是说我们之前对象s并没有变化,所以我们说String类型是不可改变的对象了,由于这种机制,每当用String操作字符串时,实际上是在不断的创建新的对象,而原来的对象就会变为垃圾被GC回收掉,可想而知这样执行效率会有多底。
而StringBuffer与StringBuilder就不一样了,他们是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,这样就不会像String一样创建一些而外的对象进行操作了,当然速度就快了。
3.一个特殊的例子:
3 StringBuffer builder = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);
你会很惊讶的发现,生成str对象的速度简直太快了,而这个时候StringBuffer居然速度上根本一点都不占优势。其实这是JVM的一个把戏,实际上:
String str = “This is only a” + “ simple” + “test”;
其实就是:
String str = “This is only a simple test”;
所以不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的String对象的话,速度就没那么快了,譬如:
String str2 = “This is only a”;
String str3 = “ simple”;
String str4 = “ test”;
String str1 = str2 +str3 + str4;
这时候JVM会规规矩矩的按照原来的方式去做。
4.StringBuilder与 StringBuffer
StringBuilder:线程非安全的
StringBuffer:线程安全的
当我们在字符串缓冲去被多个线程使用是,JVM不能保证StringBuilder的操作是安全的,虽然他的速度最快,但是可以保证StringBuffer是可以正确操作的。当然大多数情况下就是我们是在单线程下进行的操作,所以大多数情况下是建议用StringBuilder而不用StringBuffer的,就是速度的原因。
对于三者使用的总结: 1.如果要操作少量的数据用 = String
2.单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
3.多线程操作字符串缓冲区 下操作大量数据 = StringBuffer
StringBuffer常用方法
(由于StringBuffer和StringBuilder在使用上几乎一样,所以只写一个,以下部分内容网络各处收集,不再标注出处)
StringBuffer s = new StringBuffer();
这样初始化出的StringBuffer对象是一个空的对象,
StringBuffer sb1=new StringBuffer(512);
分配了长度512字节的字符缓冲区。
StringBuffer sb2=new StringBuffer(“how are you?”)
创建带有内容的StringBuffer对象,在字符缓冲区中存放字符串“how are you?”
a、append方法
public StringBuffer append(boolean b)
该方法的作用是追加内容到当前StringBuffer对象的末尾,类似于字符串的连接,调用该方法以后,StringBuffer对象的内容也发生改 变,例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.append(true);
则对象sb的值将变成”abctrue”
使用该方法进行字符串的连接,将比String更加节约内容,经常应用于数据库SQL语句的连接。
b、deleteCharAt方法
public StringBuffer deleteCharAt(int index)
该方法的作用是删除指定位置的字符,然后将剩余的内容形成新的字符串。例如:
StringBuffer sb = new StringBuffer(“KMing”);
sb. deleteCharAt(1);
该代码的作用删除字符串对象sb中索引值为1的字符,也就是删除第二个字符,剩余的内容组成一个新的字符串。所以对象sb的值变 为”King”。
还存在一个功能类似的delete方法:
public StringBuffer delete(int start,int end)
该方法的作用是删除指定区间以内的所有字符,包含start,不包含end索引值的区间。例如:
StringBuffer sb = new StringBuffer(“TestString”);
sb. delete (1,4);
该代码的作用是删除索引值1(包括)到索引值4(不包括)之间的所有字符,剩余的字符形成新的字符串。则对象sb的值是”TString”。
c、insert方法
public StringBuffer insert(int offset, boolean b),
该方法的作用是在StringBuffer对象中插入内容,然后形成新的字符串。例如:
StringBuffer sb = new StringBuffer(“TestString”);
sb.insert(4,false);
该示例代码的作用是在对象sb的索引值4的位置插入false值,形成新的字符串,则执行以后对象sb的值是”TestfalseString”。
d、reverse方法
public StringBuffer reverse()
该方法的作用是将StringBuffer对象中的内容反转,然后形成新的字符串。例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.reverse();
经过反转以后,对象sb中的内容将变为”cba”。
e、setCharAt方法
public void setCharAt(int index, char ch)该方法的作用是修改对象中索引值为index位置的字符为新的字符ch。例如:
StringBuffer sb = new StringBuffer(“abc”);
sb.setCharAt(1,’D’);
则对象sb的值将变成”aDc”。
f、trimToSize方法
public void trimToSize()
该方法的作用是将StringBuffer对象的中存储空间缩小到和字符串长度一样的长度,减少空间的浪费,和String的trim()是一样的作用,不在举例。
g、length方法
该方法的作用是获取字符串长度 ,不用再说了吧。
h、setlength方法
该方法的作用是设置字符串缓冲区大小。
StringBuffer sb=new StringBuffer();
sb.setlength(100);
如果用小于当前字符串长度的值调用setlength()方法,则新长度后面的字符将丢失。
i、sb.capacity方法
该方法的作用是获取字符串的容量。
StringBuffer sb=new StringBuffer(“string”);
int i=sb.capacity();
j、ensureCapacity方法
该方法的作用是重新设置字符串容量的大小。
StringBuffer sb=new StringBuffer();
sb.ensureCapacity(32); //预先设置sb的容量为32
k、getChars方法
该方法的作用是将字符串的子字符串复制给数组。
getChars(int start,int end,char chars[],int charStart);
StringBuffer sb = new StringBuffer("I love You");
int begin = 0;
int end = 5;
//注意ch字符数组的长度一定要大于等于begin到end之间字符的长度
//小于的话会报ArrayIndexOutOfBoundsException
//如果大于的话,大于的字符会以空格补齐
char[] ch = new char[end-begin];
sb.getChars(begin, end, ch, 0);
System.out.println(ch);
结果:I lov
String:查看源码得知,String类的声明是:public final,所以可以很清楚的知道,fianl的话是改变不了的,所以,如果我们用String来操作字符串的时候,一旦我们字符串的值改变,就会在内存创建多一个空间来保存新的字符串,可想而知,一旦遇到复杂的操作,用String是多么低效率的事啊!
所以,一般涉及到字符串操作的,我们一般使用StringBuffer或者StringBuilder,但是这两者都又有什么区别呢,下面我来说说:
查看源码可以得知:
StringBuffer和StringBuilder都集成了AbstractStringBuilder,而StringBuffer大部分方法都是synchronized,也就是线程安全的,而StringBuilder就没有,所以,我们查看API可以知道,StringBuilder可以操作StringBuffer,但是StringBuffer不可以操作StringBuilder,这也是线程的原因;
所以,可想而知,StringBuffer的效率肯定没有StringBuilder,因为StringBuffer要维持同步锁,这肯定要消耗部分资源,下面这个例子就可以充分证明这三者之间的关系:
package com.seven.exercise.StringTest; import org.junit.Test; public class StringBufferWithStringBuilder { public void testString() { long start = System.currentTimeMillis(); String str = null; for (int i = 0; i < 20000; i++) { str = str + i + ","; } System.out.println(System.currentTimeMillis() - start); } public void testStringBuffer() { long start = System.currentTimeMillis(); StringBuffer sbuf = new StringBuffer(); for (int i = 0; i < 20000; i++) { sbuf.append(i + ","); } System.out.println(System.currentTimeMillis() - start); } public void testStringBulider() { long start = System.currentTimeMillis(); StringBuilder builder = new StringBuilder(); for (int i = 0; i < 20000; i++) { builder.append(i + ","); } System.out.println(System.currentTimeMillis() - start); } @Test public void test(){ testString(); testStringBuffer(); testStringBulider(); } }
运行结果如下:
1217 9 6
效率:StringBuilder>StringBuffer>String,所以,如果这是考虑单线程程序的话,用StringBuilder,如果涉及到多线程的,那只能是StringBuffer,具体业务具体选择,要具体了解请查看API或者查看源码!
三个的源码解析:
一、String
大家经常会说使用“+”号连接String 字符串比StringBuffer慢,String类对象为不可变对象,一旦你修改了String对象的值,隐性重新创建了一个新的对象,那接下来我们详细分析一下为什么使用“+”号速度会慢,为什么String 对象是不可变对象:
1、final修饰类、引用变量、基本变量
(1)、如果一个类被final修饰则这个类是不能被继承的,没有子类。String类是一个final类,只能说明这个类不能被继承也就没有子类。
(2)、如果一个引用变量被final修饰,则引用变量的值是不能修改,而不是说被引用对象
(3)、2中说的引用变量的值的问题,如果大家看了深入理解jvm这本书的话可能对这句话有印象:由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有规定这个引用应该通过何种方式去定位,访问堆中的对象的具体位置,具体实现因不同的虚拟机而不同,有句柄地址和对象地址两种
(4)、句柄方式实现也就是在javaheap(堆内存)中分配了一个句柄池这个句柄池中存放着具体对象的地址,而引用变量中存放的是句柄池中某个句柄的地址,这种方式要查找两次地址所以速度有点慢。
(5)、对象地址方式实现,引用变量中存放的就是javaheap(堆内存)中对象的地址。
(6)、如果final修饰的是基本类型的变量则这个变量的值不能能修改,如果final修饰的是引用变量则这个引用变量的值不能改变也就是4,5 中所说的句柄的地址或对象的地址不能改变而他们所引用的对象的内容是可以改变的。
2、String 类使用"+" 来连接字符的整个过程描述
(1)、大家经常会说不要使用"+" 来连接字符串这样效率不高(相对于 StringBuilder、StringBuffer)那为什么那,看看下面:
String str= "a"; str=str+"b"; str=str+"c";
实现过程:
String str= "a";创建一个String对象,str 引用到这个对象。
在创建一个长度为str.length() 的StringBuffer 对象
StringBuffer strb=new StringBuffer(str);
调用StringBuffer的append()方法将”b“添加进去,strb.append("b");
调用strb 的toString()方法创建String对象,之前对象失去引用而str重新引用到这个新对 象。
同样在创建StringBuffer对象 调用append()方法将”c“添加进去,调用toString() 方法 创建String对象
再将strb引用到 新创建的String对象。之前对象失去引用之后存放在常量池中,等待垃圾回收。
看到上面使用“+”连接字符串的过程,就明白了为什么要使用StringBuffer 来连接字符而不是使用String 的“+”来连接。
(2)、知道了使用”+“连接的过程,我们再来看看上面提到的使用”+“号为什么会创建新的对象,也就是说String对象是不可变对象。这里有个概念就是对象不可变,而String 的对象就是一个不可变对象。那什么叫对象不可变那: 当一个对象创建完成之后,不能再修改他的状态,不能改变状态是指不能改变对象内的成员变量,包括基本数据类型的值不能改变。引用类型的变量不能指向其他对象,引用类型指向的对象的状态也不能改变。对象一旦创建就没办法修改期所有属性,所以要修改不可变对象的值就只能重新创建对象。
3、String 对象不可变 源码分析
(1)、上面说了String 对象为不可变对象,为什么String 对象不可变,String对象的状态不能改变,接下来我们看看String 类的源码:
jdk 1.7 的源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
// 数组的被final修饰,所以数据引用变量的值不能变
private final char value[];
/** Cache the hash code for the string */
// 缓存String对象的哈希值
private int hash; // Default to 0
String 类中只有两个成员变量一个是value 一个是hash,这个hash和我们讨论的问题没关系,通过注解我们知道他是缓存String对象的hash值
value 是一个被final修饰的数组对象,所以只能说他不能再引用到其他对象而不能说明他所引用的对象的内容不能改变。但我们在往下看源码就会发现String 类没有给这两个成员变量提供任何的方法所以我们也没办法修改所引用对象的内容,所以String 对象一旦被创建,这个变量被初始化后就不能再修改了,所以说String 对象是不可变对象。
(2)、String 对象不是提供了像replace()等方法可以修改内容的吗,其实这个方法内部创建了一个新String 对象 在把这个新对象重新赋值给了引用变量,看看源码你就相信了他是在内部重现创建了String 对象
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
// 创建新对象
return new String(buf, true);
}
}
return this;
}
总结:
1、String 类是一个final 修饰的类所以这个类是不能继承的,也就没有子类。
2、String 类的成员变量都是final类型的并且没有提供任何方法可以来修改引用变量所引用的对象的内容,所以一旦这个对象被创建并且成员变量初始化后这个对象就不能再改变了,所以说String 对象是一个不可变对象。
3、使用“+”连接字符串的过程产生了很多String 对象和StringBuffer 对象所以效率相比直接使用StringBuffer 对象的append() 方法来连接字符效率低很多。
4、引用变量是存在java虚拟机栈内存中的,它里面存放的不是对象,而是对象的地址或句柄地址。
5、对象是存在Java heap(堆内存)中的值
6、引用变量的值改变指的是栈内存中的这个引用变量的值的改变是,对象地址的改变或句柄地址的改变,而对象的改变指的是存放在java heap(堆内存)中的对象内容的改变和引用变量的地址和句柄没有关系。
二、StringBuffer
我们在上面说String对象是不可变的,而StringBuffer 对象是可变的,大家都说在能大体了解字符串的长度的情况下创建StringBuffer对象时 指定其容量,在上面的string中我们也知道使用“+”号的时候我们也是调用了append方法。
1、 为什么StringBuffer 对象可变, 为什么要尽量指定初始大小,append方法是怎么实现的 下面我们来看看这几个为什么
2、String 对象不可变是因为成员变量都被final修饰并且没有提供任何访问被引用对象的方法所以不能改变,而StringBuffer是怎么样的那我们可以去看看源码:
(1)、public final class StringBuffer extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
/** use serialVersionUID from JDK 1.0.2 for interoperability */
static final long serialVersionUID = 3388685877147921107L;
/**
* Constructs a string buffer with no characters in it and an
* initial capacity of 16 characters.
*/
// 默认为16个字符
public StringBuffer() {
super(16);
}
/**
* Constructs a string buffer with no characters in it and
* the specified initial capacity.
*
* @param capacity the initial capacity.
* @exception NegativeArraySizeException if the <code>capacity</code>
* argument is less than <code>0</code>.
*/
public StringBuffer(int capacity) {
super(capacity);
}
/**
* Constructs a string buffer initialized to the contents of the
* specified string. The initial capacity of the string buffer is
* <code>16</code> plus the length of the string argument.
*
* @param str the initial contents of the buffer.
* @exception NullPointerException if <code>str</code> is <code>null</code>
*/
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
3、StringBuffer 类继承自AbstractStringBuilder 那在看看AbstractStringBuilder的源码
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
// 这里我们看到,这个数组没有被final 修饰,所以引用变量的值可以改变,
//可以引用到其他数组对象
char[] value;
/**
* The count is the number of characters used.
*/
// 记录字符的个数
int count;
/**
* This no-arg constructor is necessary for serialization of subclasses.
*/
AbstractStringBuilder() {
}
/**
* Creates an AbstractStringBuilder of the specified capacity.
*/
AbstractStringBuilder(int capacity) {
// 构造函数,创建数组对象
value = new char[capacity];
}
/**
* Returns the length (character count).
*
* @return the length of the sequence of characters currently
* represented by this object
*/
public int length() {
return count;
}
3、如果知道字符串的长度则创建对象的时候尽量指定大小
(1)、在上面的源代码中我们看到StringBuffer 的构造函数默认创建的大小为16个字符。
(2)、如果我们在创建对象的时候指定了大小则创建指定容量大小的数组对象
// 调用父类的构造函数,创建数组对象
public StringBuffer(int capacity) {
super(capacity);
}
/**
* Creates an AbstractStringBuilder of the specified capacity.
*/
AbstractStringBuilder(int capacity) {
//按照指定容量创建字符数组
value = new char[capacity];
}
(3)、如果在创建对象时构造函数的参数为字符串则 创建的数组的长度为字符长度+16字符
这样的长度,然后再将这个字符串添加到字符数组中,添加的时候会判断原来字符数组中的个数加上这个字符串 的长度是否大于这个字符数组的大小如果大于则进行扩容如果没有则添加,源码如下:
public StringBuffer(String str) {super(str.length() + 16);
append(str);
}
append 出现在了这里刚好一起来看看 append方法的实现
4、其实append方法就做两件事,如果 count (字符数组中已有字符的个数)加添加的字符串的长度小于 value.length 也就是小于字符数组的容量则直接将要添加的字符拷贝到数组在修改count就可以了。
5、如果cout和添加的字符串的长度的和大于value.length 则会创建一个新字符数组 再将原有的字符拷贝到新字符数组,再将要添加的字符添加到字符数组中,再改变conut(字符数组中字符的个数)
整个添加过程的源码如下:
public synchronized StringBuffer append(Object obj) {
super.append(String.valueOf(obj));
return this;
}
public AbstractStringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
这个方法中调用了ensureCapacityInternal ()方法判断count(字符数组原有的字符个数)+str.length() 的长度是否大于value容量
/**
* This method has the same contract as ensureCapacity, but is
* never synchronized.
*/
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
如果count+str.length() 长度大于value的容量 则调用方法进行扩容
/**
* This implements the expansion semantics of ensureCapacity with no
* size check or synchronization.
*/
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
Arrays.copyOf(value,newCapacity) 复制指定的数组,截取或用 null 字符填充(如有必要),以使副本具有指定的长度。
上面的getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
将字符从此字符串复制到目标字符数组dst中,第一个参数 第二个参数截取要添加字符串的长度,第三个为目标字符数组第四个为字符串要添加到数组的开始位置
到这里数组的赋值都结束了,修改count的值,整个append也就结束了。
总结:
1、StringBuffer 类被final 修饰所以不能继承没有子类
2、StringBuffer 对象是可变对象,因为父类的 value [] char 没有被final修饰所以可以进行引用的改变,而且还提供了方法可以修改被引用对象的内容即修改了数组内容。
3、在使用StringBuffer对象的时候尽量指定大小这样会减少扩容的次数,也就是会减少创建字符数组对象的次数和数据复制的次数,当然效率也会提升。
StringBuilder 和StringBuffer 很像只是不是线程安全的其他的很像所以不罗嗦了
-
多个线程操作同一个StringBuffer对象会顺序进行(内部很多方法是同步方法)
-
多个线程操作同一个StringBuilder是同时的,这时候可能出现与预期不符合的结果
-
StringBuilder由于是线程不安全的,所以性能更好。大部分情况下都是单线程操作字符串,这时候选择StringBuilder。