文章目录
概述
String 类中有不少地方使用了 StringBuffer 和 StringBuilder 类,而这两个类都是继承自 AbstractStringBuilder 类,里面的很多实现都是直接使用父类的。
类的定义
abstract class AbstractStringBuilder implements Appendable, CharSequence
- 默认访问控制修饰符,说明只能在包内(jdk内)使用。即这个类只是给 StringBuffer 和 StringBuilder 类使用
- 是一个抽象类,只能被继承,不能直接创建对象。只有一个 toString 的抽象方法
- 实现了 Appendable 接口,Appendable 能够被追加 char 序列和值的对象。如果某个类的实例打算接收来自 Formatter 的格式化输出,那么该类必须实现 Appendable 接口,重写 append 方法
- 实现了 Charsequence 接口,代表该类,或其子类是一个字符序列
成员变量
//用于存储字符序列
char[] value;
//数组中实际存储字符的数量
int count;
这里的 value 和 String 类中的不同,是动态的,可提供给外部直接操作。
构造方法
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
AbstractStringBuilder 提供两个构造函数,一个是无参构造函数。一个是传一个 capacity(代表数组容量) 的构造,这个构造函数用于指定类中 value 数组的初始大小,数组大小后面还可动态改变。
常用方法
实现CharSequence的方法
length()
@Override
public int length() {
// 返回已经存储字符序列的实际长度
return count;
}
charAt(int)
@Override
public char charAt(int index) {
if ((index < 0) || (index >= count))
throw new StringIndexOutOfBoundsException(index);
return value[index];
}
subSequence
@Override
public CharSequence subSequence(int start, int end) {
return substring(start, end);
}
public String substring(int start, int end) {
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (end > count)
throw new StringIndexOutOfBoundsException(end);
if (start > end)
throw new StringIndexOutOfBoundsException(end - start);
return new String(value, start, end - start);
}
getValue
final char[] getValue() {
return value;
}
capacity
public int capacity() {
return value.length;
}
返回当前 value 可以存储的字符容量,即在下一次重新申请内存之前能存储字符序列的长度。新添加元素的时候,可能会对数组进行扩容。
ensureCapacity
该方法是用来确保容量至少等于指定的最小值,是该类的核心也是其两个实现类 StringBuffer 和 StringBuilder 的核心。通过这种方式来实现数组的动态扩容。下面来看下其具体逻辑。
逻辑
1.判断入参 minimumCapacity 是否有效,即是否大于 0,大于 0 执行 ensureCapacityInternal() 方法,小于等于0 则忽略。
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0)
ensureCapacityInternal(minimumCapacity);
}
2.判断入参容量值是否比原容量大,如果小于或等于原容量则忽略。如果大于原容量,执行扩容操作。
实际上就是创建一个新容量的数组,然后再将原数组中的内容拷贝到新数组中。
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value, newCapacity(minimumCapacity));
}
}
3.计算新数组的容量大小,新容量取原容量的 2 倍加 2 和入参 minCapacity 中较大者。然后再进行一些范围校验。新容量必需在 int 所支持的范围内,**之所以有 <=0 判断是因为,在执行 (value.length << 1) + 2操作后,可能会出现 int 溢出的情况。**如果溢出或是大于所支持的最大容量(MAX_ARRAY_SIZE 为 int 所支持的最大值减 8),则进行 hugeCapacity 计算,否则取 newCapacity
// 要分配的数组的最大大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
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;
}
4.先进行范围检查,必须在 int 所支持的最大范围内。然后 在minCapacity 与 MAX_ARRAY_SIZE 之间取较大者,此方法取的范围是 Integer.MAX_VALUE - 8 到 Integer.MAX_VALUE 之间的范围。
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
总结
- 通过 value = Arrays.copyOf(value,newCapacity(minimumCapacity)); 进行扩容
- 新容量取 minCapacity和原容量乘以2再加上2中较大的那个,但不能大于 Integer.MAX_VALUE - 8 。
- 在实际环境中在容量远没达到 MAX_ARRAY_SIZE 的时候就报 OutOfMemoryError 异常了,其实就是在复制的时候创建了数组 char[] copy = new char[newLength]; 这里支持不了那么大的内存消耗,可以通过
-Xms256M -Xmx768M 设置最大内存。
trimToSize
减少字符序列的使用空间,比如申请了100字符长度的空间,但是现在只用了 60 个,那剩下的 40 个无用的空间放在那里占内存,可以调用此方法释放掉未用到的内存。
public void trimToSize() {
if (count < value.length) {
value = Arrays.copyOf(value, count);
}
}
原理很简单,只申请一个 count 大小的数组把原数组中的内容复制到新数组中,原来的数组由于没有被任何引用所指向,之后会被 gc 回收。
setLength
用空字符填充未使用的空间。首先对数组进行扩容,然后将剩余未使用的空间全部填充为’0’字符。
public void setLength(int newLength) {
if (newLength < 0)
throw new StringIndexOutOfBoundsException(newLength);
ensureCapacityInternal(newLength);
if (count < newLength) {
Arrays.fill(value, count, newLength, '\0');
}
count = newLength;
}
// Arrays类中的fill方法
public static void fill(char[] a, int fromIndex, int toIndex, char val) {
rangeCheck(a.length, fromIndex, toIndex);
for (int i = fromIndex; i < toIndex; i++)
a[i] = val;
}
getChars
将字符序列中指定区间 srcBegin 到 srcEnd 内的字符拷贝到 dst 字符数组中从 dstBegin 开始往后的位置中。
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
if (srcBegin < 0)
throw new StringIndexOutOfBoundsException(srcBegin);
if ((srcEnd < 0) || (srcEnd > count))
throw new StringIndexOutOfBoundsException(srcEnd);
if (srcBegin > srcEnd)
throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
append 系列
AbstractStringBuilder 类中有一系列 append 方法,作用是在原字符序列后添加给定的对象或元素所对应的字符序列。这里挑一个代表讲解,其它方法原理类似。
public AbstractStringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
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 方法,它会在原字符序列后加上 “null” 序列(如果有必要的话会先进行扩容)。
- 如果不为 null 则进行扩容操作,最小值为 count+len,这一步可能增加容量也可能不增加。
- 将参数的字符串序列添加到 value 中。
- 最后返回 this,注意这里返回的是 this,也就意味者,可以在一条语句中多次调用 append 方法,即调用链。原理简单,但思想值得借鉴。asb.append(“hello”).append(“world”);
delete
删除字符序列指定区间的内容。如果 start==end 则不作任何操作。这个方法不改变原序列的容量。
public AbstractStringBuilder delete(int start, int end) {
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (end > count)
end = count;
if (start > end)
throw new StringIndexOutOfBoundsException();
int len = end - start;
if (len > 0) {
System.arraycopy(value, start+len, value, start, count-end);
count -= len;
}
return this;
}
deleteCharAt
删除字符序列中指定索引 index 位置的字符。
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--;
return this;
}
replace
将原字符序列指定区间 start 到 end 区间内的内容替换为 str。
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;
int len = str.length();
int newCount = count + len - (end - start);
ensureCapacityInternal(newCount);
System.arraycopy(value, end, value, start + len, count - end);
str.getChars(value, start);
count = newCount;
return this;
}
替换过程中序列长度会改变,所以需要进行扩容和改就 count 的操作。
substring
切割原字符序列指定区间 start 到 end 内的内容,返回结果子串。
public String substring(int start) {
return substring(start, count);
}
public String substring(int start, int end) {
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (end > count)
throw new StringIndexOutOfBoundsException(end);
if (start > end)
throw new StringIndexOutOfBoundsException(end - start);
return new String(value, start, end - start);
}
insert 系列
insert 系列作用是将给定对象所对应的字符串插入到原序列的指定位置。insert 系列同 append 系列类似,只不过 append 是在原序列末尾添加元素,insert 是在指定位置插入元素。这里选一个代表进行讲解。
public AbstractStringBuilder insert(int offset, String str) {
if ((offset < 0) || (offset > length()))
throw new StringIndexOutOfBoundsException(offset);
if (str == null)
str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
System.arraycopy(value, offset, value, offset + len, count - offset);
str.getChars(value, offset);
count += len;
return this;
}
indexOf
查询给定字符串在原字符序列中第一次出现的位置。
public int indexOf(String str) {
return indexOf(str, 0);
}
public int indexOf(String str, int fromIndex) {
return String.indexOf(value, 0, count, str, fromIndex);
}
lastIndexOf
返回指定字符在此字符串中最后一次出现处的索引。
public int lastIndexOf(String str) {
return lastIndexOf(str, count);
}
public int lastIndexOf(String str, int fromIndex) {
return String.lastIndexOf(value, 0, count, str, fromIndex);
}
reverse
将字符序列反转。如 “hellow” 执行 reverse 后变成 “wolleh”。
public AbstractStringBuilder reverse() {
// 判断字符序列中是否包含surrogates pair
boolean hasSurrogates = false;
int n = count - 1;
for (int j = (n-1) >> 1; j >= 0; j--) {
int k = n - j;
char cj = value[j];
char ck = value[k];
value[j] = ck;
value[k] = cj;
if (Character.isSurrogate(cj) ||
Character.isSurrogate(ck)) {
hasSurrogates = true;
}
}
if (hasSurrogates) {
reverseAllValidSurrogatePairs();
}
return this;
}
private void reverseAllValidSurrogatePairs() {
for (int i = 0; i < count - 1; i++) {
char c2 = value[i];
if (Character.isLowSurrogate(c2)) {
char c1 = value[i + 1];
if (Character.isHighSurrogate(c1)) {
value[i++] = c1;
value[i] = c2;
}
}
}
}
- 将字符反转,count 为数组长度,因为是从 0 开始的所以这里需要减 1。具体转换是第一个字符与最后一个字符对调,第二个字符与倒数第二个字符对调,依次类推
- 实际上上述操作只需要循环 (n-1) /2 + 1 次(判断条件 j>=0 所以要 +1 次,源码中 >>1 就是除以 2)就可以了,如数组长度为 9 则需要循环 (9-1-1)/2 +1 = 4 次,9 个字符对调次,第 5 个位置的字符不用换,如果长度为 10 需要循环 (10-1-1)/2 +1 = 5 次
- 剩下的工作就是两个位置的元素互换
- 如果序列中包含 surrogates pair 则执行 reverseAllValidSurrogatePairs 方法
Surrogate Pair 是 UTF-16 中用于扩展字符而使用的编码方式,采用四个字节(两个 UTF-16 编码)来表示一个字符。 char 在 java 中是 16 位的,刚好是一个 UTF-16 编码。而字符串中可能含有 Surrogate Pair ,但他们是一个单一完整的字符,只不过是用两个 char 来表示而已,因此在反转字符串的过程中 Surrogate Pairs 是不应该被反转的。而 reverseAllValidSurrogatePairs 方法就是对 Surrogate Pair 进行处理。