jdk 1.5

image.png

StringBuilder类是一个具体的类,继承了AbstractStringBuilder抽象类,实现了序列化接口和可读字符序列接口。
final修饰,不能被继承和重写里面的方法。
继承AbstractStringBuilder类,从子类的角度分析,子类继承了抽象类中具体实现的方法,StringBuilder类中的大多数方法都是直接调用父类中的方法,为什么将它们都放在一个抽象类中呢?在StringBuffer类也继承了AbstractStringBuilder抽象类,其中有一些共有的方法,如果单独写,会增加代码的冗余量,因此将共有的方法封装到一个抽象类中,如果子类需要可以直接调用,也可以重写。

一、成员属性


  1. 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);
    }

四个构造方法:

  1. 默认的构造方法,初始化了16个容量。char[] value = new char[16],可以存放16个字符。
  2. 指定容量的构造方法。
  3. 通过一个字符串构造一个StringBuilder对象。其容量为字符串的长度+16,这样做为了提高效率,在append时,判断容量时,就不需要再进行扩容操作。
  4. 通过一个可读字符序列构造一个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

image.png

在第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);
    }

拷贝流程如下:

StringBuilder - 图3

假设现有两个线程同时执行append方法,两个线程都执行完了第五行的ensureCapacityInternal方法,此刻count=5.

StringBuilder - 图4

这个时候线程1的cpu时间片用完了,线程2继续执行。线程2执行完整个append方法后count变为6.

StringBuilder - 图5
线程1继续执行第六行的str.getChars()方法的时候拿到的count值就是6了,执行char数组拷贝的时候就会抛出ArrayIndexOutOfBoundsException异常。