StringBuffer 这个类是我们日常开发中经常会使用的一个字符串操作类,该类提供了非常多的关于字符串操作相关的类,尤其是 append 方法更为常用。
1 目标本次源码分析的目标是深入了解 StringBuffer类中 append 方法的实现机制。
2 分析方法首先编写测试代码,然后利用 Intellij Idea 的单步调试功能,逐步的分析其实现思路。
测试代码如下:
StringBuffer stringBuffer = new StringBuffer(); //断点 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 no characters in it and an * initial capacity of 16 characters. */ public StringBuffer() { super(16); }
点击进入后,我们看到的是以上代码,表示调用父类的构造函数,并附上参数16。StringBuffer 的父类是谁?这个数字16又是什么意思呢?
我们再按 Shift + F7进入查看做进一步分析。
/** * Creates an AbstractStringBuilder of the specified capacity. */ AbstractStringBuilder(int capacity) { value = new char[capacity]; }
通过上述代码我们知道,StringBuffer 的父类是 AbstractStringBuilder,这是一个抽象类。其构造函数初始化了一个默认大小的字符数组,而这个字符数组的大小正是传进来的参数。
通过字符数组来保存字符串信息,为什么默认大小为16,如果字符串超过16,超过了字符数组的大小了怎么办?
我们希望通过后续的分析能够解决上面提出的这个问题。
因此,我们了解到,StringBuffer 的构造函数本质是调用了父类 AbstractStringBuilder 类的构造函数,该构造函数初始化了一个默认大小为16的字符数组。
3.2 append()方法
接下来我们分析 append 方法的实现机制。
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
从上述代码可以看到,直接调用了父类 AbstractStringBuilder 类的 append 方法。
public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, 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 characters used. */ int count;
从代码的注释我们可以看到,count 表示的是已经使用的字符数量。从§3.1节构造函数我们知道这些字符串都是存储在一个字符数组中,而 count 指的就是这个字符数组已经使用了多少个。
由于我们是第一次执行 append 方法,此前没有追加任何的字符,因此此时 count 为0,当我们追加完成后,此时 count 的值就要更新为5,表示此时的字符数组中已经有5个字符了。
所以 ensureCapacityInternal()方法的参数指的是已经使用的字符数量+将要使用的字符数量,即字符数组的最小容量大小。方法分析见§3.3节,该方法确定了新字符数组的容量,并初始化新字符数组,将原有字符数组内容复制到新字符数组中。
str.getChars()方法分析见§3.5节,主要完成将追加的字符串复制到字符数组中。
str.getChars(0, len, value, count);
将追加的字符串str的0-len位置的字符复制到 字符数组 value 的起始位置 count 处。
因此,append 方法主要的工作是:获得要追加的字符串的长度,判断当前字符数组是否能够存储追加的字符串,如果容量不够则确定新的字符数组的容量,申请新的字符数组,将以前字符数组的内容复制到新字符数组。最后将要追加的字符串,复制到新字符数组中。
3.3 ensureCapacityInternal()方法
按住 Shift+F7进入该方法的实现代码。
/** * For positive values of {@code minimumCapacity}, this method * behaves like {@code ensureCapacity}, however it is never * synchronized. * If {@code minimumCapacity} is non positive due to numeric * overflow, this method throws {@code OutOfMemoryError}. */ private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { 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 large as the given minimum capacity. * Returns the current capacity increased by the same amount + 2 if * that suffices. * Will not return a capacity greater than {@code MAX_ARRAY_SIZE} * unless the given minimum capacity is greater than that. * * @param minCapacity the desired minimum capacity * @throws OutOfMemoryError if minCapacity is less than zero or * greater than Integer.MAX_VALUE */ private int newCapacity(int minCapacity) { // overflow-conscious code int newCapacity = (value.length << 1) + 2; if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? 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 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); }
getChars()为 String 类的方法,通过调用 System.arraycopy()系统方法完成将当前字符串的 scrBegin ~ srcEnd 复制到字符数组的 dstBegin 位置。
3.6 appendNull()方法
String nullStr = null;
stringBuffer.append(nullStr);
当我们追加的是一个 null 串的时候,StringBuffer 是如何处理的。
private AbstractStringBuilder appendNull() { int c = 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 编程的技巧和一些设计思路。