一起学JDK源码 -- AbstractStringBuilde
查看所有目录
前一篇查看了String类的源码,发现String类中有不少地方使用了StringBuffer和StringBuilder类,而这两个类都是继承自AbstractStringBuilder类,里面的很多实现都是直接使用父类的,所以就看一下AbstractStringBuilder类的源码。
类的申明:
abstract class AbstractStringBuilder implements Appendable, CharSequence {}
1.默认访问控制修饰符,说明只能在包内使用,即只能在JDK内部使用,可能有人会问我创建一个java.lang包然后里面的类就可以使用AbstractStringBuilder类了,想法不错,但jkd不允许,会报SecurityException : Prohibited package name: java.lang。故这个类只是给StringBuffer和StringBuilder类使用的。
2.类名用abstract修饰说明是一个抽象类,只能被继承,不能直接创建对象。查了里面的方法你会发现它就一个抽象方法,toString方法。
3.实现了Appendable接口,Appendable能够被追加 char 序列和值的对象。如果某个类的实例打算接收来自 Formatter 的格式化输出,那么该类必须实现 Appendable 接口。
4.实现了Charsequence接口,代表该类,或其子类是一个字符序列。
成员变量:
char[] value;
int count;
value用于承装字符序列,count数组中实际存储字符的数量。这里的value同String类中的value不同,String类中的value是final的不可被修改,这里的value是动态的,并且可提供给外部直接操作。
构造函数:
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
AbstractStringBuilder提供两个构造函数,一个是无参构造函数。一个是传一个capacity(代表数组容量)的构造,这个构造函数用于指定类中value数组的初始大小,数组大小后面还可动态改变。
其它方法:
length:
public int length() {
return count;
}
返回已经存储字符序列的实际长度,即count的值。
capacity:
public int capacity() {
return value.length;
}
返回当前value可以存储的字符容量,即在下一次重新申请内存之前能存储字符序列的长度。新添加元素的时候,可能会对数组进行扩容。
ensureCapacity:
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0)
ensureCapacityInternal(minimumCapacity);
}
该方法是用来确保容量至少等于指定的最小值,是该类的核心也是其两个实现类StringBuffer和StringBuilder的核心。通过这种方式来实现数组的动态扩容。下面来看下其具体逻辑。
1.判断入参minimumCapacity是否有效,即是否大于0,大于0执行ensureCapacityInternal方法,小于等于0则忽略。
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
2.判断入参容量值是否比原容量大,如果大于原容量,执行扩容操作,实际上就是创建一个新容量的数组,然后再将原数组中的内容拷贝到新数组中,如果小于或等于原容量则忽略。
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;
}
3.计算新数组的容量大小,新容量取原容量的2倍加2和入参minCapacity中较大者。然后再进行一些范围校验。新容量必需在int所支持的范围内,之所以有<=0判断是因为,在执行 (value.length << 1) + 2操作后,可能会出现int溢出的情况。如果溢出或是大于所支持的最大容量(MAX_ARRAY_SIZE为int所支持的最大值减8),则进行hugeCapacity计算,否则取newCapacity
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
4.这一步先进行范围检查,必须在int所支持的最大范围内。然后在minCapacity与MAX_ARRAY_SIZE之间取较大者,此方法取的范围是Integer.MAX_VALUE - 8到Integer.MAX_VALUE之间的范围。
5.总结:
1.通过value = Arrays.copyOf(value,newCapacity(minimumCapacity));进行扩容
2.新容量取 minCapacity,原容量乘以2再加上2中较大的,但不能大于int所支持的最大范围。
3.在实际环境中在容量远没达到MAX_ARRAY_SIZE的时候就报OutOfMemoryError异常了,其实就是在复制的时候创建了数组char[] copy = new char[newLength];这里支持不了那么大的内存消耗,可以通过 -Xms256M -Xmx768M设置最大内存。
trimToSize:
public void trimToSize() {
if (count < value.length) {
value = Arrays.copyOf(value, count);
}
}
减少字符序列的使用空间,比如申请了100字符长度的空间,但是现在只用了60个,那剩下的40个无用的空间放在那里占内存,可以调用此方法释放掉未用到的内存。原理很简单,只申请一个count大小的数组把原数组中的内容复制到新数组中,原来的数组由于没有被任何引用所指向,之后会被gc回收。
setLength:
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;
}
用空字符填充未使用的空间。首先对数组进行扩容,然后将剩余未使用的空间全部填充为'0'字符。
charAt:
public char charAt(int index) {
if ((index < 0) || (index >= count))
throw new StringIndexOutOfBoundsException(index);
return value[index];
}
获取字符序列中指定位置的字符,范围为0到count,超出范围抛StringIndexOutOfBoundsException异常。
codePointAt:
public int codePointAt(int index) {
if ((index < 0) || (index >= count)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointAtImpl(value, index, count);
}
获取字符序列中指定位置的字符,所对应的代码点,即ascii码。
codePointBefore:
public int codePointBefore(int index) {
int i = index - 1;
if ((i < 0) || (i >= count)) {
throw new StringIndexOutOfBoundsException(index);
}
return Character.codePointBeforeImpl(value, index, 0);
}
获取字符序列中指定位置的前一个位置的字符,所对应的代码点。
codePointCount:
public int codePointCount(int beginIndex, int endIndex) {
if (beginIndex < 0 || endIndex > count || beginIndex > endIndex) {
throw new IndexOutOfBoundsException();
}
return Character.codePointCountImpl(value, beginIndex, endIndex-beginIndex);
}
获取字符串代码点个数,是实际上的字符个数。不清楚代码点可查看java中代码点与代码单元的区别。
public int offsetByCodePoints(int index, int codePointOffset) {
if (index < 0 || index > value.length) {
throw new IndexOutOfBoundsException();
}
return Character.offsetByCodePointsImpl(value, 0, value.length,
index, codePointOffset);
}
返回此字符序列中从给定的index处偏移codePointOffset个代码点的索引。不清楚代码点的可以查看java中代码点与代码单元的区别。
getChars:
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);
}
将字符序列中指定区间srcBegin到srcEnd内的字符拷贝到dst字符数组中从dstBegin开始往后的位置中。
setCharAt:
public void setCharAt(int index, char ch) {
if ((index < 0) || (index >= count))
throw new StringIndexOutOfBoundsException(index);
value[index] = ch;
}
设置字符序列中指定索引index位置的字符为ch。
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;
}
AbstractStringBuilder类中有一系列append方法,作用是在原字符序列后添加给定的对象或元素所对应的字符序列。这里挑一个代表讲解,其它方法原理类似。
1.首先判断所传参数是否为null,如果为null则调用appendNull方法,实际上就是在原字符序列后加上"null"序列。
2如果不为null则进行扩容操作,最小值为count+len,这一步可能增加容量也可能不增加,当count+len小于或等于capacity就不用进行扩容。
3.然后再将参数的字符串序列添加到value中。
4.最后返回this,注意这里返回的是this,也就意味者,可以在一条语句中多次调用append方法,即大家所知的方法调用链。原理简单,但思想值得借鉴。asb.append("hello").append("world");
appendCodePoint:
public AbstractStringBuilder appendCodePoint(int codePoint) {
final int count = this.count;
if (Character.isBmpCodePoint(codePoint)) {
ensureCapacityInternal(count + 1);
value[count] = (char) codePoint;
this.count = count + 1;
} else if (Character.isValidCodePoint(codePoint)) {
ensureCapacityInternal(count + 2);
Character.toSurrogates(codePoint, value, count);
this.count = count + 2;
} else {
throw new IllegalArgumentException();
}
return this;
}
添加代码点,将入参转换为对应的代码点后,添加到原字符序列结尾。不清楚代码点的可以查看java中代码点与代码单元的区别。
delete:
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:
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;
}
删除字符序列中指定索引index位置的字符。
replace:
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;
}
将原字符序列指定区间start到end区间内的内容替换为str,替换过程中序列长度会改变,所以需要进行扩容和改就count的操作。
substring:
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);
}
切割原字符序列指定区间start到end内的内容,返回字符串形式。
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;
}
insert系列作用是将给定定对象所对应的字符串插入到原序列的指定位置。insert系列同append系列类似,只不过append是在原序列末尾添加元素,insert是在指定位置插入元素。这里也选一个代表进行讲解。
假设原字符序列为"hello"现调用insert(int 1, “aa");
1.对待插入的位置offset进行检查,必须在容量内
2.如果传入对象为null则插入"null"字符串
3.对value数组进行扩容
4.通过System.arraycopy对数组进行复制
arraycopy(Object src,int srcPos,Object dest,int destPos,int length);
src:源数组; ['h','e','l','l','o','o']
srcPos:源数组要复制的起始位置;1
dest:目的数组; ['h','e','l','l','o','w']
destPos:目的数组放置的起始位置; 1+2=3
length:复制的长度。 6-1=5
则执行完这句后value中的内容为['h','e','l','e','l','l','o','o']
可以看到是将位置1到结尾的内容后移了两个长度,因为需要插入的字符串"bb"的长度为2
5.将str的内容复制到value中
str.getChars(value, offset);//将str的内容复制到value中从offset 1位置开始复制
复制完成后['h','b','b','e','l','l','o','o'],即我们最终想要达到的效果"hbbellow"
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);
}
查询给定字符串在原字符序列中第一次出现的位置。调用的其实是String类的indexOf方法,具体可查看一起学JDK源码 -- String类
reverse:
public AbstractStringBuilder reverse() {
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;
}
该方法用于将字符序列反转,如"hellow"执行reverse后变成"wolleh"。
1.hasSurrogates用于判断字符序列中是否包含surrogates pair
2.将字符反转,count为数组长度,因为是从0开始的所以这里需要减1。具体转换是第一个字符与最后一个字符对调,第二个字符与倒数第二个字符对调,依次类推
3.实际上上述操作只需要循环(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次
4.剩下的工作就是两个位置的元素互换。
5.如果序列中包含surrogates pair 则执行reverseAllValidSurrogatePairs方法
reverseAllValidSurrogatePairs:
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;
}
}
}
}
Surrogate Pair是UTF-16中用于扩展字符而使用的编码方式,是一种采用四个字节(两个UTF-16编码)来表示一个字符。
char在java中是16位的,刚好是一个UTF-16编码。而字符串中可能含有Surrogate Pair,但他们是一个单一完整的字符,只不过是用两个char来表示而已,因此在反转字符串的过程中Surrogate Pairs 是不应该被反转的。而reverseAllValidSurrogatePairs方法就是对Surrogate Pair进行处理。
toString:
public abstract String toString();
这是这个抽象类中唯一的一个抽象方法,需要子类去实现。
查看所有目录