coding……
但行好事 莫问前程

Java编程拾遗『StringBuilder和StringBuffer』

在之前的文章中,我讲了String类,在讲intern那一节中,讲到当String实例使用”+”操作时,编译后其实使用的是通过StringBuilder的append实现的。另外我们都知道String是不可变类,每次对String实例进行改变的时候,都会生成一个新的String实例,然后将引用指向新的String实例,所以经常改变内容的字符串最好不要用String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM的 GC就会开始工作,性能就会降低,这就是为什么要引入StirngBuilder和StringBuffer的原因,因为StringBuilder内容是可变的,对字符串修改时,并不会生成新的对象。

StringBuilder和StringBuffer类,这两个类的方法基本是完全一样的,它们的实现代码也几乎一样,唯一的不同就在于,StringBuffer是线程安全的,而StringBuilder不是。使用时,每次都在对象本身进行操作,而不是生成新的对象并改变对象引用。所以在字符串内容需要经常改动的情况下,建议使用StringBuilder和StringBuffer。本文会详细介绍一下StringBuilder的常用方法和实现,对于StringBuffer就不多讲了,可以简单的理解为是StringBuilder中添加线程同步的实现。

1. StringBuilder

StringBuilder是Java 5引入的一个新的类,用于提供与StringBuffer API兼容但是非同步的实现,实现字符串被单个线程使用时更高效地处理。在Java API中定义如下:

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
        
	//……
	//
}

继承自抽象类AbstractStringBuilder,AbstractStringBuilder抽象类内部维护了一个char数组,并实现了数组的append、delete、insert、reverse、扩容等操作。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
      /**
       * The value is used for character storage.
      */
      char[] value;

      /**
       * The count is the number of characters used.
      */
      int count;

      //……
}

1.1 Java API常用方法说明

方法 说明
public StringBuilder()
public StringBuilder(CharSequence seq)
public StringBuilder(int capacity)
public StringBuilder(String str)
构造函数
public StringBuilder append(boolean b)
public StringBuilder append(char c)
public StringBuilder append(char[] str)
public StringBuilder append(char[] str, int offset, int len)
public StringBuilder append(CharSequence s)
public StringBuilder append(CharSequence s, int start, int end)
public StringBuilder append(double d)
public StringBuilder append(float f)
public StringBuilder append(int i)
public StringBuilder append(long lng)
public StringBuilder append(Object obj)
public StringBuilder append(String str)
public StringBuilder append(StringBuffer sb)
public StringBuilder appendCodePoint(int codePoint)
append操作
public StringBuilder delete(int start, int end) 删除内部char数组index start到end位置的内容,区间左闭右开
public StringBuilder deleteCharAt(int index) 删除内部char数组index位置的内容
public int indexOf(String str) 返回str在StringBuilder内部char数组第一次出现的索引位置
public int indexOf(String str, int fromIndex) 返回str在StringBuilder内部char数组从fromIndex开始第一次出现的索引位置
public StringBuilder insert(int offset, boolean b)
public StringBuilder insert(int offset, char c)
public StringBuilder insert(int offset, char[] str)
public StringBuilder insert(int dstOffset, CharSequence s)
public StringBuilder insert(int offset, double d)
public StringBuilder insert(int offset, float f)
public StringBuilder insert(int offset, int i)
public StringBuilder insert(int offset, long l)
public StringBuilder insert(int offset, Object obj)
public StringBuilder insert(int offset, String str)
在StringBuilder内部char数组offset开始插入对象
比如:new StringBuilder(“zhuoli”).insert(1, “卓”) -> “z卓huoli”
public StringBuilder insert(int index, char[] str, int offset, int len) 在StringBuilder内部char数组index开始,插入str从offset开始长度为len的元素
public StringBuilder insert(int dstOffset, CharSequence s,
int start, int end)
在StringBuilder内部char数组dstOffset开始,插入s从start到end区间的元素,区间左闭右开
public int lastIndexOf(String str) 返回str在StringBuilder内部char数组最后一次出现的索引位置
public int lastIndexOf(String str, int fromIndex) 返回str在StringBuilder内部char数组从fromIndex开始最后一次出现的索引位置
public StringBuilder replace(int start, int end, String str) 将StringBuilder内部char数组从index start到end区间的内容替换为str
public StringBuilder reverse() 将StringBuilder内部char数组反转,对于四字节字符,做了特殊处理
public String toString() toString

1.2 原理及实现

1.2.1 构造函数

上面讲过StringBuilder继承自AbstractStringBuilder,AbstractStringBuilder内部封装了一个char数组,这一点跟String很像,所以StringBuilder可以像String一样表示字符串。但与String不同的是,String内部的char数组时final修饰的,表示引用不可修改,但AbstractStringBuilder内部的char数组时非final修饰的,数组的引用地址可以修改(比如当数组容量不够扩容时,引用地址是可以改变的)。另外AbstractStringBuilder内部的char数组,所有位置都是被使用的(length为数组的长度,同时可以表示数组中元素的个数),但是AbstractStringBuilder中的char数组不一定所有位置都已经被使用,所以AbstractStringBuilder内部又维护了一个count字段,表示char数组元素的个数。

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

public StringBuilder(int capacity) {
    super(capacity);
}

AbstractStringBuilder(int capacity) {
    value = new char[capacity];
}

可以看到StringBuilder的构造函数最终都通过调用AbstractStringBuilder的构造函数,对数组容量进行了初始化,比如当使用StringBuilder无参构造函数时,就初始化了一个容量为16的char数组,count的默认值为0。

1.2.2 append

append顾名思义就是追加的意思,append重载的方法很多,这里不一一解释了,看一个比较常用的方法,如下:

@Override
public StringBuilder append(String str) {
    super.append(str);
    return this;
}

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会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩容。具体来说,ensureCapacityInternal(count+len)会确保数组的长度足以容纳新添加的字符,str.getChars会拷贝新添加的字符到字符数组中,count更新为append后的实际长度。下面看一下扩容的具体实现:

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0) {
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}

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;
}

注意这里如果容量不够,使用的扩容方式是Arrays.copyOf,会生成一个新的数组,这就是之前讲的AbstractStringBuider内部的char数组非final的原因。然后讲一下扩容策略,minCapacity是需要的最小长度,这里并没有直接使用minCapacity,而是将原数组长度 * 2 + 2进行扩展,是为了避免频繁进行扩容,影响效率。如果事先知道大概需要的长度,建议使用如下这个构造方法:

public StringBuilder(int capacity)

1.2.3 delete

@Override
public StringBuilder delete(int start, int end) {
    super.delete(start, end);
    return this;
}

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;
}

可以看到delete操作,实际是通过System.arraycopy实现的,就是将数组后面的内容复制到前面来,很简单这里不多讲了,复制后的数组的引用仍是value,也就是讲只是修改了数组的内容,并没有修改数组的地址。其实StringBuilder的大多数操作都是通过System.arrayCopy实现的。

1.2.4 insert

@Override
public StringBuilder insert(int offset, String str) {
    super.insert(offset, str);
    return this;
}

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操作是在StringBuilder内部char数组,插入一个字符串str,插入开始的位置是offset。可以看到,实现方式是,先通过System.arraycopy将数组元素往后移str.length()个位置,然后通过str.getChars操作,将str拷贝到数组中去。同样要注意StringBuilder包含四字节字符的特例,如下:

StringBuilder stringBuilder = new StringBuilder("xx");
System.out.println(stringBuilder.insert(1, "haha"));  //结果是 xhahax

StringBuilder stringBuilder1 = new StringBuilder("?");
System.out.println(stringBuilder1.insert(1, "haha"));  //结果是 ?haha?

原因很简单,因为insert操作将四字节字符的高位代理和低位代理切断了,形成了两个非法字符,所以为结果?haha?

1.2.5 replace

@Override
public StringBuilder replace(int start, int end, String str) {
    super.replace(start, end, str);
    return this;
}

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;
}

replace操作将StringBuilder内部char数组index start到end区间的内容替换为str,start – end的大小跟str.length()无必然联系。实现原理就是通过System.arraycopy先将数组往后移str.length()个长度,然后通过str.getChars操作将数组复制进去。

1.2.6 reverse

@Override
public StringBuilder reverse() {
    super.reverse();
    return this;
}

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;
}

reverse为翻转字符串操作,如果StringBuilder中存在四字节字符,reverse会进行特殊处理 ,保证翻转后四字节字符仍是有效的

StringBuilder stringBuilder = new StringBuilder("a?b");
System.out.println(stringBuilder.reverse()); //结果为"b?a"

2. StringBuffer

StringBuffer和StringBuilder一样,继承与AbstractStringBuilder,并对父类AbstractStringBuilder的方法都覆盖实现同步,从而实现线程安全。但是从另一个角度讲,因为synchronized同步时比较耗性能的,所以在确定多线程使用的情况下可以考虑使用StringBuilder。下面举个例子,append在StringBuffer中的实现:

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

可以看到方法被声明为synchronized,所以是线程安全的。另外要提一下的是toStringCache,toStringCache 是StringBuffer中维护的一个char数组,如下:

/**
 * A cache of the last value returned by toString. Cleared
 * whenever the StringBuffer is modified.
 */
private transient char[] toStringCache;

看注释就可以明白,它是一个缓存,所有修改操作(比如上面的append)都会将这个cache清空。toString时,如果这个缓存为空的时候,就知道StringBuffer被修改过了,这时候就会通过AbstractStringBuilder中的char数组同步toStringCache内容。因为它是一个缓存,所以没必要在对象中持久化,所以使用transient修饰。

@Override
public synchronized String toString() {
    if (toStringCache == null) {
        toStringCache = Arrays.copyOfRange(value, 0, count);
    }
    return new String(toStringCache, true);
}

 

赞(0) 打赏
Zhuoli's Blog » Java编程拾遗『StringBuilder和StringBuffer』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址