jdk 1.5
StringBuilder类是一个具体的类,继承了AbstractStringBuilder抽象类,实现了序列化接口和可读字符序列接口。
final修饰,不能被继承和重写里面的方法。
继承AbstractStringBuilder类,从子类的角度分析,子类继承了抽象类中具体实现的方法,StringBuilder类中的大多数方法都是直接调用父类中的方法,为什么将它们都放在一个抽象类中呢?在StringBuffer类也继承了AbstractStringBuilder抽象类,其中有一些共有的方法,如果单独写,会增加代码的冗余量,因此将共有的方法封装到一个抽象类中,如果子类需要可以直接调用,也可以重写。
一、成员属性
static final long serialVersionUID = 4383685877147921099L;
可序列化的一个标识。
因为StringBuilder类是AbstractStringBuilder类的子类,所以继承了value和count两个成员属性。这两个属性在AbstractStringBuilder类中做了介绍。
char[] value;
int count;
二、构造方法
public StringBuilder() {
super(16);
}
public StringBuilder(int capacity) {
super(capacity);
}
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
四个构造方法:
- 默认的构造方法,初始化了16个容量。char[] value = new char[16],可以存放16个字符。
- 指定容量的构造方法。
- 通过一个字符串构造一个StringBuilder对象。其容量为字符串的长度+16,这样做为了提高效率,在append时,判断容量时,就不需要再进行扩容操作。
- 通过一个可读字符序列构造一个StringBuilder对象,其容量也为该字符序列的长度+16。这样做的目的和上一个一致。
三、主要方法
4.1 toString()
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
重写了Object类中的toString方法。其实toString方法在AbstractStringBuilder抽象类中声明了,并且在CharSequence接口中也声明了,在StringBuilder、StringBuffer类中必须实现toString方法。
创建了一个新的字符串对象,代码中注释说了创建一个副本,而不要去共享这个内部维护的数组,因为返回的是String对象,不可变的,如果返回了数组的共享,在改变StringBuilder对象时,String对象的内容随之改变,这就破坏了String对象的不可变性。
4.2 reverse()
@Override
public StringBuilder reverse() {
super.reverse();
return this;
}
重写了父类AbstractStringBuilder抽象类的具体reverse方法,调用的是父类中的方法,返回当前对象。
4.3 replace(int,int,String)
@Override
public StringBuilder replace(int start, int end, String str) {
super.replace(start, end, str);
return this;
}
重写了父类AbstractStringBuilder抽象类的具体replace方法,调用的是父类中的方法,返回当前对象。
4.4 append(String)
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
重写了父类AbstractStringBuilder抽象类的具体append方法,调用的是父类中的方法,返回当前对象。
StringBuilder类重写了AbstractStringBuilder的方法如下:
- indexOf(String,int)
- lastIndexOf(String)
- lastIndexOf(String,int)
- insert(int,Object)
- delete(int,int)
- …..
四、其它方法
4.1 writeObject(ObjectOutputStream)
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.defaultWriteObject();
s.writeInt(count);
s.writeObject(value);
}
在进行序列化的时候保存StringBuilder对象的状态到一个流中。
4.2 readObject(ObjectInputStream)
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
count = s.readInt();
value = (char[]) s.readObject();
}
在反序列化时从流中获取StringBuilder对象序列化之前的状态。
关于更多序列化内容,后期见。
五、面试常见问题:
5.1 字符串拼接原理
@Test
public void test04() {
String str = "ab";
String str1 = "c";
String string = str + str1;
System.out.println(string);
}
这段代码的执行流程,我们先将代码编译后,进行反编译,查看其做了什么。执行javap -v xxx.class
在第10标号中表明创建了一个StringBuilder对象。然后+时调用了append方法,最后调用StringBuilder中的toString方法,返回一个String。这是针对字符串变量进行连接操作的底层实现过程。
5.2 StringBuilder不安全的点在哪?
先创建10个多线程操作StringBuilder。每个线程循环1000次向StringBuilder对象里面append字符。正常情况打印10000,实际呢?
public class ThreadBuilder{
public static void main(String[] args) throws InterruptedException {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
builder.append(i);
}
}
}).start();
}
Thread.sleep(1000);
System.out.println(builder.length());
}
}
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException
at java.lang.System.arraycopy(Native Method)
at java.lang.String.getChars(String.java:826)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:449)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at ThreadBuilder$1.run(ThreadBuilder.java:16)
at java.lang.Thread.run(Thread.java:748)
9767
我们看到输出的数值小于10000,还抛出了ArrayIndexOutOfBoundsException异常(不是必须的)。
为什么输出的值会小于10000?
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;
}
append方法的最终逻辑,在第7行中,count += len不是一个原子操作。假设这个时候count值为10,len值为1,两个线程同时执行到第7行,拿到的count值都是10,执行完加法运算后将结果赋值给count,所以两个线程执行完后count为11,而不是12。这就是为什么输出的值会小于10000了。
为什么会抛出ArrayIndexOutOfBoundsException异常?
依旧看append方法,在其第5行ensureCapacityInternal方法,检查StringBuilder对象的原char数组的容量能不能装下新的字符串。装不下会有一个扩容的机制。
void expandCapacity(int minimumCapacity) {
//计算新的容量
int newCapacity = value.length * 2 + 2;
//中间省略了一些检查逻辑
...
value = Arrays.copyOf(value, newCapacity);
}
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
//拷贝数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
AbstractStringBuilder的append方法的中第六行,是将String对象里面char数组里面的内容拷贝到StringBuilder对象的char数组中。
str.getChars(0, len, value, count);
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
//中间省略了一些检查
...
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
拷贝流程如下:
假设现有两个线程同时执行append方法,两个线程都执行完了第五行的ensureCapacityInternal方法,此刻count=5.
这个时候线程1的cpu时间片用完了,线程2继续执行。线程2执行完整个append方法后count变为6.
线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。