1.定义
StringBuilder是从jdk1.5提供出来的新的String封装类,而StringBuffer是jdk1.0就已经存在了,这两个类的拼接效率远高于string。
2.String简介
String其实就是由若干个字符线性排列而成的,可以理解为字符Array,那么既然是数组实现的,那就需要考虑到数组的特性,数组在内存中是一块连续的地址空间块,即在定义数组的时候需要指定数组的大小
换言之, 数组就分为可变数组和不可变数组。可变数组能够动态插入和删除,而不可变数组一旦分配好空间后则不能进行动态插入或删除操作。
在实际的字符串应用场景中,涉及到多种操作,比如字符串的插入,删除,修改,拼接,查询,替换…
3.效率比对
现在我们对这三个类进行30w次的拼接,每次加上“-a”,测试效率,代码如下:
package com.hqa.design.test;
public class Test {
public static void main(String[] args) {
String str = "test";
StringBuffer sbf = new StringBuffer(str);
StringBuilder sbl = new StringBuilder(str);
//string
testString(str);
//stringBuilder
testStringBuilder(sbl);
//stringBuffer
testStringBuffer(sbf);
}
/**
* StringBuffer拼接
* @param str
*/
private static void testStringBuffer(StringBuffer str) {
long start = System.currentTimeMillis();
for(int i = 0;i < 300000;i++){
str.append("-a");
}
System.out.println("StringBuffer拼接,耗时:" + (System.currentTimeMillis() - start) + "ms");
}
/**
* StringBuilder拼接
* @param str
*/
private static void testStringBuilder(StringBuilder str) {
long start = System.currentTimeMillis();
for(int i = 0;i < 300000;i++){
str.append("-a");
}
System.out.println("StringBuffer拼接,耗时:" + (System.currentTimeMillis() - start) + "ms");
}
/**
* 原生String拼接
* @param str
*/
private static void testString(String str) {
long start = System.currentTimeMillis();
for(int i = 0;i < 300000;i++){
str += "-a";
}
System.out.println("String拼接,耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
输出:
String拼接,耗时:72122ms
StringBuilder拼接,耗时:5ms
StringBuffer拼接,耗时:9ms
可以看到 拼接效率 StringBuilder > StringBuffer >> String
4.String类详解
我们都知道string是不可变类,属性value为不可变数组,即String初始化构造器没有初始容量为16的概念,你定义多少,String中字符数组的长度就是多少,不存在字符数组扩容一说。
看一下类图:
我们来看一下string类的源码:
//类由final修饰,表明不可被继承
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
//我们可以看到其内部是一个final修饰的char数组
//这里的value变量其实就是存储了String字符串中的所有字符。
private final char value[];
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
public String() {
this.value = new char[0];
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
既然String,不可变。我们再看下它的截取方法subString()实现
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
...
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
//数组的拷贝方法就是重新构造一个新的char数组
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
很明显,从源码我们可以发现,如果截取长度等于元字符串,那么返回this,否则就会重新new一个新的string对象。
类似的我们可以看到,String类的concat方法,replace方法,都是内部重新生成一个String对象的。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
...
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;
}
这也就是为什么我们如果采用String对象频繁的进行拼接,截取,替换操作效率很低下的原因。
5.StringBuilder类详解
内部可变数组,存在初始化StringBuilder对象中字符数组容量为16,存在扩容。
同样先看一下类图:
StringBuilder继承自父类AbstractStringBuilder,其内部核心方法很多都是直接调用自父类。
构造函数:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;//此处的char数组没有final修饰,表明可以重新赋值,这一点与String类不同
....
....
/**
* Constructs a string builder with no characters in it and an
* initial capacity of 16 characters.
*/
public StringBuilder() {
super(16);
}
父类AbstractStringBuilder的构造:
/**
* Creates an AbstractStringBuilder of the specified capacity.
*/
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
可以看到父类初始化了一个长度为16的char数组,除此以外StringBuilder还提供了初始容量大小的含参构造器
/**
* Constructs a string builder with no characters in it and an
* initial capacity specified by the {@code capacity} argument.
*
* @param capacity the initial capacity.
* @throws NegativeArraySizeException if the {@code capacity}
* argument is less than {@code 0}.
*/
public StringBuilder(int capacity) {
super(capacity);
}
以字符串String 作为参数的构造器
/**
* Constructs a string builder initialized to the contents of the
* specified string. The initial capacity of the string builder is
* {@code 16} plus the length of the string argument.
*
* @param str the initial contents of the buffer.
*/
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
在参数Str 数组长度的基础上再增加16个字符长度,作为StringBuilder实例的初始数组容量,并将str字符串 append到StringBuilder的数组中。
我们在看下父类的append方法
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len); //扩容逻辑
str.getChars(0, len, value, count); //原char数组拷贝到新的数组,这行比较重要,看下面
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) //如果当前需要的长度大于当前char数组长度,则进行扩容
expandCapacity(minimumCapacity);
}
/**
*其具体的扩容逻辑,如果当前value数据的长度小于需要的最小长度,就会进行一次扩容,
*先将现有的数组长度乘以二倍再加2,如果还是小于最小需要的长度,则扩容的长度为传入
*的minCapacity,即最小需要的长度。最后还会判断最小需要的长度是否已经大于数组长度
的最大值获取小于等于0,如果是的话就会取最大数组长度。
*/
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);
}
看下append方法的关键:String的 getChars方法(从str的0位开始,到str的长度,当前StringBuilder对象的字符数组,当前数组已有的字符长度)
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
//
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
//System class
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
其实是调用了System的arraycopy方法 参数如下:
value 为str的内部不可变字符数组,
srcBegin 为从str 字符串数组的0下标开始,
srcEnd 为str字符串数组的长度,
dst 为StringBuilder对象的内部可变字符数组,
dstBegin 则为StringBuilder对象中已有的字符长度(char[] 已有的元素长度)
即整个StringBuilder的append方法,本质上是调用System的native方法,直接将String 类型的str字符串中的字符数组,拷贝到了StringBuilder的字符数组中
最后看一下stringBuilder的toString()方法
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
这里的toString方法直接new 一个String对象,将StringBuilder对象的value进行一个拷贝,重新生成一个对象,不共享之前StringBuilder的char[]
可以发现没有像StringBuilder一样去重新new 对象,所以在频繁的拼接字符上,StringBuilder的效率远远高于String类。
6.StringBuffer
同样搞一张类图
其实StringBuffer和StringBuilder非常类似,可以说是StringBuilder的翻版,但是StringBuffer是在jdk1.0就已经存在了,所以其实StringBuilder的设计应该是借鉴自StringBuffer
看下append方法:
@Override
public synchronized StringBuffer append(CharSequence s) {
toStringCache = null;
super.append(s);
return this;
}
可以看到这里就是在append方法上加了同步锁,来实现多线程下的线程安全。其他的和StringBuilder一致。
多了一个toStringCache全局变量,这里的作用简单介绍一下,就是去缓存toString的。
可以看下StringBuffer的toString方法
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
这里的作用就是如果StringBuffer对象此时存在toStringCache,在多次调用其toString方法时,其new出来的String对象是会共享同一个char[] 内存的,达到共享的目的。但是StringBuffer只要做了修改,其toStringCache属性值都会置null处理。这也是StringBuffer和StringBuilder的一个区别点。
7.总结
String 类不可变,内部维护的char[] 数组长度不可变,为final修饰,String类也是final修饰,不存在扩容。字符串拼接,截取,都会生成一个新的对象。频繁操作字符串效率低下,因为每次都会生成新的对象。
StringBuilder 类内部维护可变长度char[] , 初始化数组容量为16,存在扩容, 其append拼接字符串方法内部调用System的native方法,进行数组的拷贝,不会重新生成新的StringBuilder对象。非线程安全的字符串操作类, 其每次调用 toString方法而重新生成的String对象,不会共享StringBuilder对象内部的char[],会进行一次char[]的copy操作。
StringBuffer 类内部维护可变长度char[], 基本上与StringBuilder一致,但其为线程安全的字符串操作类,大部分方法都采用了Synchronized关键字修改,以此来实现在多线程下的操作字符串的安全性。其toString方法而重新生成的String对象,会共享StringBuffer对象中的toStringCache属性(char[]),但是每次的StringBuffer对象修改,都会置null该属性值。