本文首发于微信公众号:"算法与编程之美",欢迎关注,及时了解更多此系列博客。
StringBuffer这个类是我们日常开发中经常会使用的一个字符串操作类,该类提供了非常多的关于字符串操作相关的类,尤其是append方法更为常用。
1目标
本次源码分析的目标是深入了解StringBuffer类中append方法的实现机制。
2分析方法
首先编写测试代码,然后利用Intellij Idea的单步调试功能,逐步的分析其实现思路。
测试代码如下:
StringBuffer stringBuffer =newStringBuffer();//断点
stringBuffer.append("hello");
stringBuffer.append("hello11");
stringBuffer.append("hello22");
String nullStr = null;
stringBuffer.append(nullStr);
3分析流程
点击调试按钮,开始分析流程。
3.1构造函数
首先进行的是构造函数的分析,点击F7进入构造函数实现。
此时需要注意的是,当我们点击F7发现Idea无响应并未进入构造函数内部实现。这是为什么?
Idea的F7 (step into)默认是不进入JDK的类实现,而StringBuffer类正是JDK中lang包下的类,因此点击F7并未跳到内部实现。此时应该选择Shift+F7(force step into)按键。
/**
* Constructs a string buffer with nocharacters in it and an
* initial capacity of 16 characters.
*/
publicStringBuffer(){
super(16);
}
点击进入后,我们看到的是以上代码,表示调用父类的构造函数,并附上参数16。StringBuffer的父类是谁?这个数字16又是什么意思呢?
我们再按Shift + F7进入查看做进一步分析。
/**
* Creates an AbstractStringBuilder ofthe specified capacity.
*/
AbstractStringBuilder(intcapacity) {
value=new char[capacity];
}
通过上述代码我们知道,StringBuffer的父类是AbstractStringBuilder,这是一个抽象类。其构造函数初始化了一个默认大小的字符数组,而这个字符数组的大小正是传进来的参数。
通过字符数组来保存字符串信息,为什么默认大小为16,如果字符串超过16,超过了字符数组的大小了怎么办?
我们希望通过后续的分析能够解决上面提出的这个问题。
因此,我们了解到,StringBuffer的构造函数本质是调用了父类AbstractStringBuilder类的构造函数,该构造函数初始化了一个默认大小为16的字符数组。
3.2 append()方法
接下来我们分析append方法的实现机制。
@Override
publicsynchronizedStringBuffer append(String str) {
toStringCache=null;
super.append(str);
return this;
}
从上述代码可以看到,直接调用了父类AbstractStringBuilder类的append方法。
publicAbstractStringBuilder append(String str) {
if(str ==null)
returnappendNull();
intlen = str.length();
ensureCapacityInternal(count+ len);
str.getChars(, len,value,count);
count+= len;
return this;
}
首先判断追加的字符串是否为null,如果为null则执行appendNull()方法。下一节我们再分析这个方法。
根据上一节测试代码编写的:stringBuffer.append("hello");
我们追加的是一个字符串"hello",并非null值。
获取追加字符串的长度len值为5。
下面将进入ensureCapacityInternal ()方法,该方法的参数为count+len = 0 + 5 = 5.
这个count属性是什么意思呢?
将鼠标放在count上面,点击Ctrl+B进入到该属性的定义:
/**
* The count is the number of charactersused.
*/
intcount;
从代码的注释我们可以看到,count表示的是已经使用的字符数量。从§3.1节构造函数我们知道这些字符串都是存储在一个字符数组中,而count指的就是这个字符数组已经使用了多少个。
由于我们是第一次执行append方法,此前没有追加任何的字符,因此此时count为,当我们追加完成后,此时count的值就要更新为5,表示此时的字符数组中已经有5个字符了。
所以ensureCapacityInternal()方法的参数指的是已经使用的字符数量+将要使用的字符数量,即字符数组的最小容量大小。方法分析见§3.3节,该方法确定了新字符数组的容量,并初始化新字符数组,将原有字符数组内容复制到新字符数组中。
str.getChars()方法分析见§3.5节,主要完成将追加的字符串复制到字符数组中。
str.getChars(, len,value,count);
将追加的字符串str的0-len位置的字符复制到字符数组value的起始位置count处。
因此,append方法主要的工作是:获得要追加的字符串的长度,判断当前字符数组是否能够存储追加的字符串,如果容量不够则确定新的字符数组的容量,申请新的字符数组,将以前字符数组的内容复制到新字符数组。最后将要追加的字符串,复制到新字符数组中。
3.3 ensureCapacityInternal()方法
按住Shift+F7进入该方法的实现代码。
/**
* For positive values of {@codeminimumCapacity},this method
* behaves like {@codeensureCapacity},however it is never
* synchronized.
* If {@codeminimumCapacity} isnon positive due to numeric
* overflow, this method throws {@codeOutOfMemoryError}.
*/
privatevoidensureCapacityInternal(intminimumCapacity) {
// overflow-conscious code
if(minimumCapacity -value.length>) {
value= Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
通过§3.1节构造函数的分析,我们了解到StringBuffer的底层是使用字符数组来存储这些字符串的,而且默认的大小是16,一旦这个字符数组用完了,就得重新分配新的字符数组,并将以前的字符数组内容复制到新的字符数组中。
minimumCapacity指的就是当前字符串的最小容量,如果这个容量比当前字符数组的容量要大,则需要重新申请新的字符数组,并将以前字符数组的内容复制到新的字符数组中。
那么新的字符数组容量是多少呢?
答案就在newCapacity(minimumCapacity)方法中,见§3.4节。
有了新的字符数组了以后,接下来就需要将以前的字符数组的内容复制到新的字符数组,通过Arrays.copyOf()方法实现。
因此,ensureCapacityInternal()完成的工作主要是确定新的字符数组的大小并将旧字符数组的内容复制到新字符数组中。
3.4 newCapacity()方法
按住Shift+F7进入该方法的实现。
/**
* Returns a capacity at least as largeas the given minimum capacity.
* Returns the current capacity increasedby the same amount + 2 if
* that suffices.
* Will not return a capacity greaterthan {@codeMAX_ARRAY_SIZE}
* unless the given minimum capacity isgreater than that.
*
*@paramminCapacitythe desired minimumcapacity
*@throwsOutOfMemoryError ifminCapacity is less than zero or
* greater than Integer.MAX_VALUE
*/
privateintnewCapacity(intminCapacity) {
// overflow-conscious code
intnewCapacity = (value.length
if(newCapacity - minCapacity
newCapacity = minCapacity;
}
return(newCapacity
? hugeCapacity(minCapacity)
: newCapacity;
}
默认的newCapacity大小是原有的字符数组大小左移一位加上2,即2*oldCapacity+2,将原有的字符数组扩大一倍再加上2。为什么是这样的一种算法呢?直接左移一位不就可以了吗?为什么还要加2?
这里面的newCapacity和minCapacity两个变量容易产生混淆,其中newCapacity指的是字符数组新的容量大小,而minCapacity指的是当前要存储字符串而需要的最小容量。因此要想能够存储当前字符串,就必须保证newCapacity >=minCapacity。
所以上述源码中加了一个判断newCapacity是否大于minCapacity,如果不是则newCapacity的大小直接设置为minCapacity。
最后返回的时候,还加上了相关判断信息,当newCapacity超过了当前数组的最大值的时候,执行hugeCapacity()方法。
3.5 str.getChars()方法
按住Shift+F7进入该方法的实现代码:
public voidgetChars(intsrcBegin,intsrcEnd,chardst[],intdstBegin) {
if(srcBegin
throw newStringIndexOutOfBoundsException(srcBegin);
}
if(srcEnd >value.length) {
throw newStringIndexOutOfBoundsException(srcEnd);
}
if(srcBegin > srcEnd) {
throw newStringIndexOutOfBoundsException(srcEnd- srcBegin);
}
System.arraycopy(value, srcBegin, dst,dstBegin, srcEnd - srcBegin);
}
getChars()为String类的方法,通过调用System.arraycopy()系统方法完成将当前字符串的scrBegin ~ srcEnd复制到字符数组的dstBegin位置。
3.6 appendNull()方法
String nullStr = null;
stringBuffer.append(nullStr);
当我们追加的是一个null串的时候,StringBuffer是如何处理的。
privateAbstractStringBuilder appendNull() {
intc =count;
ensureCapacityInternal(c +4);
final char[] value =this.value;
value[c++] ='n';
value[c++] ='u';
value[c++] ='l';
value[c++] ='l';
count= c;
return this;
}
这是一个私有的方法。首先确保容量足够,其次我们看到所谓的null本质就是追加了'null'这样的四个字符到字符数组中。
4总结
本文分析了StringBuffer类的append方法,通过分析我们知道append方法的所有工作都是由父类AbstractStringBuilder完成的。基本的思路是检查当前字符数组的容量是否足够,如果不够,则申请新的字符数组,然后将原有字符数组的内容复制到新的字符数组。最后将追加的字符串复制到新的字符数组后面,从而完成追加操作。
如果让你来设计一个类来完成字符串的不断追加操作,你会怎么设计呢?
可能大部分同学想到的都是每次追加的时候都进行扩容,申请一个新的字符数组,将原有数组的内容复制到新数组,最后将追加的字符串复制到新数组后面。
这种实现方式最大的问题就是效率。不停的进行数组的复制操作导致效率非常低下,因此StringBuffer提出的思路是每次我多申请一些字符数组,当容量不够的时候,申请原有容量2倍+2的容量,而不仅仅是满足minCapacity最小容量的大小。这就是提升效率的一种方式,这种设计方式在很多场景都有应用。
通过源码分析,我们深入了解一个类的内部实现机制,使得我们今后会更加高效的使用这个类。另外我们还会学习到一些Java编程的技巧和一些设计思路。
欢迎持续关注“算法与编程之美”微信公众号,了解更多。
领取专属 10元无门槛券
私享最新 技术干货