AbstractStringBuilder
是一个可变的字符序列类的抽象类,最初出现于jdk1.5中。
历史
众所周知java最常用的字符数据类型是Stirng对象,但String对象被设计为了一个常量类(内部的维护 final char[]),每次变更都会产生新的对象。在需要频繁更换字符值的场景下,很浪费系统资源。为了解决这个问题可变字符的对象家族诞生了。 (StringBuffer+StringBuilder) 而 AbstractStringBuilder现在是他们的爸爸。
为什么加粗了"现在"这个词呢?
- 因最先出现的是StringBuffer 它诞生于jdk.10中(灵异事件这个时候他现在的爸爸还没出生),他是一个线程安全类。后来我们聪明的java开发团队发现实际应用中大多数场景下是不需要线程安全的
- 所以在jdk1.5中 小爸爸AbstractStringBuilder(封装了一些公用方法)和小弟弟StringBuilder(非线程安全类)诞生了
类图
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
}
AbstractStringBuilder 实现了两个接口。我们简单介绍下他的两个干爹
- CharSequence 一个可读序列类接口,string也实现了他。我们可以理解为他规范了java中字符类型对象的基础方法 (int length(); char charAt(int index); public String toString(); 等)
- Appendable
可追加的字符序列类接口,我们可以理解为 CharSequence规范了字符操作,Appendable规范了字符追加操作 ,截止jdk8中它之规定了 append方法和他的重载方法
核心成员
- char[] value;
这个很好理解用于存储字符类型的 value数组 - int count;
这个用来表示 char[] 字符长度。问题来了:为什么不用char数组的value.length来表示字符长度呢?带着疑问我们往下看
常用方法
/**
* 追加字符串 str
**/
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;
}
/**
* char数组扩容
* @param minimumCapacity 期望的最小数组长度
*/
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) {
// 当前长度小于最小期望 进行扩容
value = Arrays.copyOf(
value,
newCapacity(minimumCapacity) // 新的数组长度由 newCapacity方法产生
); // 产生新的数组
}
}
/**
* @param minCapacity 最小期望长度
*/
private int newCapacity(int minCapacity) {
// overflow-conscious code
// 先取当前数组长度 乘2在加2, “为了减少扩容次”数每次扩容长度最起码都要翻倍(不然每次都扩容的话不久和String一样了)。
// 为什么是成“二再加二”? 不知道!有人知道么?
// jdk注解很少告诉我为什么,但应该是经过思考的选择. 有兴趣可以深究一下
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity; // 上一步操作后还不满足 最小期望,那就使用最小期望值来作为新的数组长度
}
// 当新长度, 小于零或者 大于数组最大长度(MAX_ARRAY_SIZE)的时候,交给hugeCapacity方法选择新数组长度
// MAX_ARRAY_SIZE 有意思了 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 为什么最大长度等于 int最大长度 - 8 呢?
// java中外定义数组时传入数组长度参数类型为int所以无法定义超过Integer.MAX_VALUE长度的数组(编译时就会报错)
// 那为什么要 - 8 呢 ?
// jdk注释是这么写的:
// Some VMs reserve some header words in an array.
// Attempts to allocate larger arrays may result in
// OutOfMemoryError
// 一些虚拟机在数组中保留一些空间,尝试分配较大的数组可能会导致内存溢出错误! 所以减了个8。
// 不过注意 当程序尝试分配的大小 在 MAX_ARRAY_SIZE 》 Integer.MAX_VALUE 之间时 hugeCapacity方法还是会返回期望值。否则返回 MAX_ARRAY_SIZE 或者抛出异常
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
/**
* 最小期望长度 大于 Integer.MAX_VALUE 最大值时抛出异常
* @param minCapacity 最小期望长度
* @return 返回 MAX_ARRAY_SIZE(数组最大长度) 或者 返回minCapacity( minCapacity > MAX_ARRAY_SIZE)
*/
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
/**
* 这个方法还是留着子类自己去实现了
*/
@Override
public abstract String toString();
解释大部分在注释里 这里系统的整理一下
整理下上面的问题
- 为什么不用char数组的value.length来表示字符长度呢?
看过上面的源码以及注释可以知道 char[] 每次扩容都是由newCapacity方法决定的。这里有个机制为了防止每次操作字符串都要扩容char数组,每次扩容时都尝试多开辟些空闲空间。下次更改字符时会先用空闲空间 不够用在扩容 所以数组长度大于等于实际内容长度,那么这里需要单独一个字段记录实际内容长度了
一个引申问题我们常用的 HotSpot java虚拟机中数组最大长度到底是多少?
执行这段代码
public static void main(String[] args) {
int i = Integer.MAX_VALUE;
while (true) {
try {
System.out.println(new char[i].length);
} catch (OutOfMemoryError e) {
i--;
e.printStackTrace();
// 异常继续
}
System.out.println("数组最大长度 十进制:" + i);
System.out.println("数组最大长度 二进制:" + Integer.toBinaryString(i));
System.out.println("数组最大长度 二进制位数:" + Integer.toBinaryString(i).length());
return;
}
}
会输入什么样的内容呢?
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at com.tlong.TestAbstracString.main(TestAbstracString.java:10)
数组最大长度 十进制:2147483646
数组最大长度 二进制:1111111111111111111111111111110
数组最大长度 二进制位数:31
理论上数组最大长度应该是2^31
- 二进制31位最大值
- 虽然java规定数组对象头信息中用于记录数组长度的空间大小为 32bit, 但是。java中int大小为4字节32位,其中有一位表示正负号。所以int正整数最大值是2^31
事实上HotSpot1.8 下数组最大长度是 (2^31 - 1)
- 就像注释里说的一些虚拟机在数组中保留一些空间,尝试分配较大的数组可能会导致内存溢出错误