StringBuilder与StringBuffer,估计面试被问这两个的问题应该很常见了。但只答出线程安不安全那是远远不够的。
这个两个东西的出现大多人应该都知道,因为String不可变,如果想强行可变,那么会导致一直创建新的String对象。StringBuilder,StringBuffer这两个就可以动态添加字符串。那么它们是怎么实现的呢,下面就从源码来进一步认识它们。
因为String不可变,Java 引入了可变字符串变量 StringBuilder 类,但是它不是线程安全的,只用在单线程场景下。
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequencee
通过源码可以看到,StringBuilder继承自AbstractStringBuilder。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
在AbstractStringBuilder可以看到这个两个属性,这是实现StringBuilder中最重要的属性。
value 该数组用于存储字符串值。
count 表示该字符串对象中已使用的字符数。
public StringBuilder() { //默认容量大小为16
super(16);
}
public StringBuilder(int capacity) { //传入一个int值,指定容量的大小
super(capacity);
}
public StringBuilder(String str) { //传入String类型,容量为传入的字符串长度加上16
super(str.length() + 16);
append(str);
}
public StringBuilder(CharSequence seq) { //传入CharSequence,传入参数为 CharSequence 类型时也跟上面一样做相同处理
this(seq.length() + 16);
append(seq);
}
有多个append
方法,都只是传入的参数不同而已,下面挑几个典型的深入看看,其他都是类似的处理。
public StringBuilder append(String str) {
super.append(str); //调用父类的append方法
return this;
}
public AbstractStringBuilder append(String str) {
if (str == null) //如果传入的是null,则直接调用appendNull方法
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len); //根据判断是否需要扩容进行扩容操作
str.getChars(0, len, value, count); //合并数据
count += len; //增加已使用的字符数
return this;
}
源码我已加上注释,继续跟进去,下面看ensureCapacityInternal方法
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) { //判断所需要的长度是否大于当前长度,是则扩容
value = Arrays.copyOf(value, //扩容后将原来的数组复制过来
newCapacity(minimumCapacity)); //newCapacity 扩容的方法
}
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2; //扩大一倍再加上2
if (newCapacity - minCapacity < 0) { //如果还是不够大则直接等于需要的容量大小
newCapacity = minCapacity;
} //如果为负数,则设置为MAX_ARRAY_SIZE
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
跟到这里,我们可以知道,newCapacity
方法用于确定新容量大小,将现有容量大小扩大一倍再加上2,如果还是不够大则直接等于需要的容量大小。如果新容量大小为负则容量设置为MAX_ARRAY_SIZE
,它的大小等于Integer.MAX_VALUE - 8
。
继续看getChars方法,前面做了一些基本的越界判断,最后其实里面就是使用arraycopy方法进行数组复制数据。
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);
}
在这里也顺便也介绍一下arraycopy这个方法吧
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
Object src : 原数组
int srcPos : 从元数据的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length : 要copy的数组的长度
该方法用于将指定范围的字符删掉,delete方法比较简单,看源码
public StringBuilder delete(int start, int end) {
super.delete(start, end);
return this;
}
public AbstractStringBuilder delete(int start, int end) {
if (start < 0) //判断是否越界
throw new StringIndexOutOfBoundsException(start);
if (end > count) //end 不能大于已使用字符数 count,大于的话则令其等于 count
end = count;
if (start > end) //判断是否越界
throw new StringIndexOutOfBoundsException();
int len = end - start;
if (len > 0) { //判断完后使用arraycopy方法,把 end 后面的字符串复制到 start 位置,即相当于将中间的字符删掉
System.arraycopy(value, start+len, value, start, count-end);
count -= len; //最后修改count值,减去删除的长度。
}
return this;
}
public AbstractStringBuilder replace(int start, int end, String str) {
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (start > count)
throw new StringIndexOutOfBoundsException("start > length()");
if (start > end)
throw new StringIndexOutOfBoundsException("start > end");
if (end > count) //end 不能大于已使用字符数 count,大于的话则令其等于 count
end = count;
int len = str.length();
int newCount = count + len - (end - start); //计算出新count
ensureCapacityInternal(newCount); //判断是否需要扩容
System.arraycopy(value, end, value, start + len, count - end);
str.getChars(value, start);
count = newCount; //更新count
return this;
}
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
该方法用于将指定范围的字符替换成指定字符串。ensureCapacityInternal方法跟System.arraycopy可参照前面。
此方法可以删除指定索引位置的元素。
public AbstractStringBuilder deleteCharAt(int index) {
if ((index < 0) || (index >= count)) //判断是否越界
throw new StringIndexOutOfBoundsException(index);
System.arraycopy(value, index+1, value, index, count-index-1); //数据操作
count--; //因为是删除一个,所以count-1
return this;
}
类似的,也是操作arraycopy实现。
StringBuilder主要方法就介绍到这里。
很多人都知道StringBuffer是线程安全的,那它是怎么实现的?
其实就是jdk提供的synchronized锁,它在对操作数据的方法加了同步锁。
StringBuffer跟Stringbuilder的方法实现差不多,只是多了一个锁。所以这里就提供append方法跟delete方法,看源码:
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
public synchronized StringBuffer delete(int start, int end) {
toStringCache = null;
super.delete(start, end);
return this;
}
可以看到StringBuffer的源码中,主要是通过在必要的方法上加 synchronized 来实现线程安全。
1、其实StringBuffer 和 StringBuilder 其实实现逻辑几乎都一样,并且抽象到 AbstractStringBuilder 抽象类中来实现,只是 StringBuffer 将一些必要的方法进行同步处理了。
2、使用上看程序是否需要关注线程安全,如果是单线程的话那就直接可以用StringBuilder,因为没有锁的抢占,效率快。如果需要考虑线程安全问题,那么就要使用StringBuffer。
3、System.arraycopy主要是这个方法在操作的数据的增减,掌握它是怎么实现复制操作数据的。
4、自己主动去跟一遍源码才是最好的。
最后想说,现在许多程序员做了两三年,基础方面还是很薄弱,认为一直学新框架提升自己,其实这不完全对的,这样就像是虚胖子,还是应该得把基础打好,内功练好。大多人看一个程序员的技术怎样,肯定是先问基础。