Java 中 StringBuilder 在高性能用法总结

关于StringBuilder,一般同学只简单记住了,字符串拼接要用StringBuilder,不要用+,也不要用StringBuffer,然后性能就是最好的了,真的吗吗吗吗?

项目开发中很多时候我们都需要拼接字符串,那如何才能高效的完成字符串拼接呢?

字符串,就是一系列字符的集合。
   Java里面提供了String,StringBuffer和StringBuilder三个类来封装字符串,其中StringBuilder类是到jdk
1.5才新增的。字符串操作可以说是几乎每门编程语言中所必不可少的,你真的理解其内幕吗?
【欢迎各位大神批评指正,转载请保留链接……谢谢!】
下面让我们开始探秘之旅吧!

还有些同学,还听过三句似是而非的经验:

指定初始容量

先来看一下StringBuilder的源码(JDK7)

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

    /** use serialVersionUID for interoperability */
    static final long serialVersionUID = 4383685877147921099L;

    /**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {
        super(16);
    }

    /**
     * Constructs a string builder with no characters in it and an
     * initial capacity specified by the <code>capacity</code> argument.
     *
     * @param      capacity  the initial capacity.
     * @throws     NegativeArraySizeException  if the <code>capacity</code>
     *               argument is less than <code>0</code>.
     */
    public StringBuilder(int capacity) {
        super(capacity);
    }
}

StringBuilder的默认构造方法调用的是父类AbstractStringBuilder
中的AbstractStringBuilder(int capacity)构造方法,如下:

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;

    /**
     * This no-arg constructor is necessary for serialization of subclasses.
     */
    AbstractStringBuilder() {
    }

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
}

StringBuilder的内部有一个char[],
在调用StringBuilder的无参构造方法时其内部char[]的默认长度是16。当我们调用StringBuilder的append方法时,其实就是不断的往char[]里填东西的过程。

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

其中,super.append是调用AbstractStringBuilder 的append(String
str)方法,如下:

public AbstractStringBuilder append(String str) {
    if (str == null) str = "null";
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

StringBuilder的扩容和ArrayList有些类似,具体代码如下:

/**
 * This method has the same contract as ensureCapacity, but is
 * never synchronized.
 */
private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0)
        expandCapacity(minimumCapacity);
}

/**
 * This implements the expansion semantics of ensureCapacity with no
 * size check or synchronization.
 */
void expandCapacity(int minimumCapacity) {
    int newCapacity = value.length * 2 + 2;
    if (newCapacity - minimumCapacity < 0)
        newCapacity = minimumCapacity;
    if (newCapacity < 0) {
        if (minimumCapacity < 0) // overflow
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;
    }
    value = Arrays.copyOf(value, newCapacity);
}

StringBuilder默认长度是16,然后,如果要append第17个字符,怎么办?
答案是采用 Arrays.copyOf()成倍复制扩容!
扩容的性能代价是很严重的:一来有数组拷贝的成本,二来原来的char[]也白白浪费了要被GC掉。可以想见,一个129字符长度的字符串,经过了16,32,64,
128四次的复制和丢弃,合共申请了496字符的数组,在高性能场景下,这几乎不能忍。

由此可见,合理设置一个初始值多重要。使用之前先仔细评估一下要保存的字符串最大长度。

1、既然都是用来封装字符串的,那为什么还要3个类来封装呢?
  1. Java编译优化后+和StringBuilder的效果一样;

  2. StringBuilder不是线程安全的,为了“安全”起见最好还是用StringBuffer;

  3. 永远不要自己拼接日志信息的字符串,交给slf4j来。

  4. 初始长度好重要,值得说四次。

复用StringBuilder

StringBuilder.setLength()方法只重置它的count指针,而char[]则会继续重用,源码如下:

public void setLength(int newLength) {
    if (newLength < 0)
        throw new StringIndexOutOfBoundsException(newLength);
    ensureCapacityInternal(newLength);

    if (count < newLength) {
        for (; count < newLength; count++)
            value[count] = '';
    } else {
        count = newLength;
    }
}

toString()方法:

public String toString() {
    // Create a copy, don't share the array
    return new String(value, 0, count);
}

而toString()时会把当前的count指针也作为参数传给String的构造函数,所以不用担心把超过新内容大小的旧内容也传进去了。可见,StringBuilder是完全可以被重用的。

具体示例如下:

String[] history_steps = ruleDetailInfo.getHistory_steps().split(",");
for (String step : history_steps){
    sb.setLength(0);
    sb.append(ruleDetailInfo.getBusiness_id()).append("t").append(step);
    results.add(sb.toString());
}
2、它们三者之间到底有何区别?

+ 与 StringBuilder的区别

String s = "hello" + user.getName();

这一行代码经过javac编译后的效果,的确等价于使用StringBuilder,但没有设定长度。

String s = new StringBuilder().append(“hello”).append(user.getName());

但是,如果像下面这样:

 String s = “hello ”;
// 中间插入了其他一些代码
s = s + user.getName();

每一条语句,都会生成一个新的StringBuilder,这里就有了两个StringBuilder,性能就完全不一样了。

如果是在循环体里s+=i; 就更加多得没谱,例如:

String str = "";
for(int i=0; i<10000;i++){
    str += i;
}
3、它们三者之间的使用场景分别是什么?

StringBuilder的内部有一个char[],
不断的append()就是不断的往char[]里填东西的过程。

StringBuffer 与 StringBuilder区别

StringBuffer的源码如下(JDK7):

public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence {

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    static final long serialVersionUID = 3388685877147921107L;

    public StringBuffer() {
        super(16);
    }

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

    public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }

    public StringBuffer(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

    public synchronized int length() {
        return count;
    }

    public synchronized int capacity() {
        return value.length;
    }

    public synchronized void ensureCapacity(int minimumCapacity) {
        if (minimumCapacity > value.length) {
            expandCapacity(minimumCapacity);
        }
    }

    public synchronized void setLength(int newLength) {
        super.setLength(newLength);
    }

    public synchronized StringBuffer append(Object obj) {
        super.append(String.valueOf(obj));
        return this;
    }

    public synchronized StringBuffer append(String str) {
        super.append(str);
        return this;
    }
    ......
}

StringBuilder源码:

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

    /** use serialVersionUID for interoperability */
    static final long serialVersionUID = 4383685877147921099L;

    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 append(Object obj) {
        return append(String.valueOf(obj));
    }

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

    // Appends the specified string builder to this sequence.
    private StringBuilder append(StringBuilder sb) {
        if (sb == null)
            return append("null");
        int len = sb.length();
        int newcount = count + len;
        if (newcount > value.length)
            expandCapacity(newcount);
        sb.getChars(0, len, value, count);
        count = newcount;
        return this;
    }

    public StringBuilder append(StringBuffer sb) {
        super.append(sb);
        return this;
    }
    ......
}

StringBuffer与StringBuilder都是继承于AbstractStringBuilder,唯一的区别就是StringBuffer的函数上都有synchronized关键字。

4、它们三者之间从内存角度来看又是怎么来实现的呢?

new StringBuilder()
时char[]的默认长度是16,然后,如果要append第17个字符,怎么办?

小结

StringBuilder是非线程安全的,所以不能在多线程环境下共享使用。StringBuilder在使用的时候一定要指定其初始大小,另外,对性能要求比较高的场景下,可以考虑用一个ThreadLocal
缓存可重用的StringBuilder。

5、它们三者之间的性能效率是怎么排列的?

下面就让我们逐一破解这几个谜团吧!

用System.arraycopy成倍复制扩容!!!!

一、概述

字符串是由若干个字符线性排列组成的,所以我们可以把字符串当作数组(Array)来看待。众所周知,数组就是内存里线性排列的有序地址空间块。既然是线性排列,有序的一组地址块,那么在分配数组空间时就必须指定空间大小也就是数组大小,这也是大多数编程语言里规定在定义数组时要指定数组大小的原因(某些编程语言中可变数组的实现另当别论)。换言之,数组就分为可变数组和不可变数组。可变数组能够动态插入和删除,而不可变数组一旦分配好空间后则不能进行动态插入或删除操作。

这样一来有数组拷贝的成本,二来原来的char[]也白白浪费了要被GC掉。可以想见,一个129字符长度的字符串,经过了16,32,64,
128四次的复制和丢弃,合共申请了496字符的数组,在高性能场景下,这几乎不能忍。

二、从实际应用可能的场景中分析String,StringBuilder,StringBuffer产生的背景

在实际应用当中我们可能会对字符串经常做如下几种操作:插入,删除,修改,拼接,截取,查到,替换……其中,“插入”和“删除”操作就涉及到对原字符串的长度进行修改(其实,“拼接”和“截取”也分为可以理解为插入和删除操作)。
    然而,在jdk
1.0中就出现的String类封装的字符串是不可变的,即不能在原字符串上进行插入删除操作,String的API是通过新建临时变量的方式来实现字符串的插入和删除操作。因为是新建临时变量,所以当你调用String类的API对字符串进行插入和删除操作时原来的字符串是不会有任何改变的,返回给你的是一个新的字符串对象。关于这一点,我们将在后面通过分析源码的方式来证实!既然String类封装的是不可变数组,那么对应的就应该有一个类来封装可变数组。没错!StringBuffer类封装的就是可变数组,并且还是线程安全的。所以,在非多线程环境下效率相对较低。正如大家所想的一样,JDK里也提供了非多线程环境下使用的可变字符串的封装类,它就是在jdk
1.5里面才姗姗来迟的StringBuilder类,StringBuilder类是非线程安全的可变字符串封装类,也是我们今天要讨论的成员之一。
    到此,我们基本上就已经回答了在文章开始处提出的第1,2,3个问题。接下来,我们再从源码的角度来更深入讨论。

所以,合理设置一个初始值多重要。

三、从源码角度进一步探讨其内部实现方式

但如果我实在估算不好呢?多估一点点好了,只要字符串最后大于16,就算浪费一点点,也比成倍的扩容好。

1、String类的关键源码分析如下:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
   private final char value[];//final类型char数组
//省略其他代码……
……
}

从上述的代码片段中我们可以看到,String类在类开始处就定义了一个final
类型的char数组value。也就是说通过String类定义的字符串中的所有字符都是存储在这个final
类型的char数组中的。

下面我们来看一下String类对字符串的截取操作,关键源码如下:

public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
       //当对原来的字符串进行截取的时候(beginIndex >0),返回的结果是新建的对象
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }

当我们对字符串从第beginIndex(beginIndex >0)
个字符开始进行截取时,返回的结果是重新new出来的对象。所以,在对String类型的字符串进行大量“插入”和“删除”操作时会产生大量的临时变量。

2. Liferay的StringBundler类

Liferay的StringBundler类提供了另一个长度设置的思路,它在append()的时候,不急着往char[]里塞东西,而是先拿一个String[]把它们都存起来,到了最后才把所有String的length加起来,构造一个合理长度的StringBuilder。

2、StringBuffer和StringBuilder类关键源码分析:

在进行这两个类的源码分析前,我们先来分析下一个抽象类AbstractStringBuilder,因为,StringBuffer和StringBuilder都继承自这个抽象类,即AbstractStringBuilder类是StringBuffer和StringBuilder的共同父类。AbstractStringBuilder类的关键代码片段如下:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;//一个char类型的数组,非final类型,这一点与String类不同

    /**
     * This no-arg constructor is necessary for serialization of subclasses.
     */
    AbstractStringBuilder() {
    }

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];//构建了长度为capacity大小的数组
    }

//其他代码省略……
……
}

StringBuffer类的关键代码如下:

public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
   /**
     * Constructs a string buffer with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuffer() {
        super(16);//创建一个默认大小为16的char型数组
    }

    /**
     * Constructs a string buffer with no characters in it and
     * the specified initial capacity.
     *
     * @param      capacity  the initial capacity.
     * @exception  NegativeArraySizeException  if the {@code capacity}
     *               argument is less than {@code 0}.
     */
    public StringBuffer(int capacity) {
        super(capacity);//自定义创建大小为capacity的char型数组
    }
//省略其他代码……
……

StringBuilder类的构造函数与StringBuffer类的构造函数实现方式相同,此处就不贴代码了。
   下面来看看StringBuilder类的append方法和insert方法的代码,因StringBuilder和StringBuffer的方法实现基本上一致,不同的是StringBuffer类的方法前多了个synchronized关键字,即StringBuffer是线程安全的。所以接下来我们就只分析StringBuilder类的代码了。StringBuilder类的append方法,insert方法都是Override
父类AbstractStringBuilder的方法,所以我们直接来分析AbstractStringBuilder类的相关方法。

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
       //调用下面的ensureCapacityInternal方法
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
           //调用下面的expandCapacity方法实现“扩容”特性
            expandCapacity(minimumCapacity);
    }

   /**
     * This implements the expansion semantics of ensureCapacity with no
     * size check or synchronization.
     */
    void expandCapacity(int minimumCapacity) {
       //“扩展”的数组长度是按“扩展”前数组长度的2倍再加上2 byte的规则来扩展
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        //将value变量指向Arrays返回的新的char[]对象,从而达到“扩容”的特性
        value = Arrays.copyOf(value, newCapacity);
    }

从上述代码分析得出,StringBuilder和StringBuffer的append方法“扩容”特性本质上是通过调用Arrays类的copyOf方法来实现的。接下来我们顺藤摸瓜,再分析下Arrays.copyOf(value,
newCapacity)这个方法吧。代码如下:

 public static char[] copyOf(char[] original, int newLength) {
        //创建长度为newLength的char数组,也就是“扩容”后的char 数组,并作为返回值
        char[] copy = new char[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;//返回“扩容”后的数组变量
    }

其中,insert方法也是调用了expandCapacity方法来实现“扩容”特性的,此处就不在赘述了。
接下来,分析下delete(int start, int end)方法,代码如下:

 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) {
            //调用native方法arraycopy对value数组进行复制操作,然后重新赋值count变量达到“删除”特性
            System.arraycopy(value, start+len, value, start, count-end);
            count -= len;
        }
        return this;
    }

从源码可以看出delete方法的“删除”特性是调用native方法arraycopy对value数组进行复制操作,然后重新赋值count变量实现的
最后,来看下substring方法,源码如下 :

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);
        //根据start,end参数创建String对象并返回
        return new String(value, start, end - start);
    }

3. 但,还是浪费了一倍的char[]

浪费发生在最后一步,StringBuilder.toString()

// Create a copy, don't share the array
return new String(value, 0, count);

String的构造函数会用
System.arraycopy()复制一把传入的char[]来保证安全性不可变性,如果故事就这样结束,StringBuilder里的char[]还是被白白牺牲了。

为了不浪费这些char[],一种方法是用Unsafe之类的各种黑科技,绕过构造函数直接给String的char[]和count属性赋值,但很少人这样做。

另一个靠谱一些的办法就是重用StringBuilder。而重用,还解决了前面的长度设置问题,因为即使一开始估算不准,多扩容几次之后也够了。

四、总结:

4. 重用StringBuilder

这个做法来源于JDK里的BigDecimal类(没事看看JDK代码多重要),SpringSide里将代码提取成StringBuilderHolder,里面只有一个函数

public StringBuilder getStringBuilder() {
     sb.setLength(0);
     return sb;
}

StringBuilder.setLength()函数只重置它的count指针,而char[]则会继续重用,而toString()时会把当前的count指针也作为参数传给String的构造函数,所以不用担心把超过新内容大小的旧内容也传进去了。可见,StringBuilder是完全可以被重用的。

为了避免并发冲突,这个Holder一般设为ThreadLocal,标准写法见BigDecimal或StringBuilderHolder的注释。

1、String类型的字符串对象是不可变的,一旦String对象创建后,包含在这个对象中的字符系列是不可以改变的,直到这个对象被销毁。

5. + 与 StringBuilder

String s = “hello ” + user.getName();

这一句经过javac编译后的效果,的确等价于使用StringBuilder,但没有设定长度。

String s = new StringBuilder().append(“hello”).append(user.getName());

但是,如果像下面这样:

String s = “hello ”;
// 隔了其他一些语句
s = s + user.getName();

每一条语句,都会生成一个新的StringBuilder,这里就有了两个StringBuilder,性能就完全不一样了。如果是在循环体里s+=i;
就更加多得没谱。

据R大说,努力的JVM工程师们在运行优化阶段,
根据+XX:+OptimizeStringConcat(JDK7u40后默认打开),把相邻的(中间没隔着控制语句)
StringBuilder合成一个,也会努力的猜长度。

所以,保险起见还是继续自己用StringBuilder并设定长度好了。

2、StringBuilder和StringBuffer类型的字符串是可变的,不同的是StringBuffer类型的是线程安全的,而StringBuilder不是线程安全的

6. StringBuffer 与 StringBuilder

StringBuffer与StringBuilder都是继承于AbstractStringBuilder,唯一的区别就是StringBuffer的函数上都有synchronized关键字。

那些说StringBuffer
“安全”的同学,其实你几时看过几个线程轮流append一个StringBuffer的情况???

3、如果是多线程环境下涉及到共享变量的插入和删除操作,StringBuffer则是首选。如果是非多线程操作并且有大量的字符串拼接,插入,删除操作则StringBuilder是首选。毕竟String类是通过创建临时变量来实现字符串拼接的,耗内存还效率不高,怎么说StringBuilder是通过JNI方式实现终极操作的。

7. 永远把日志的字符串拼接交给slf4j??

logger.info("Hello {}", user.getName());

对于不知道要不要输出的日志,交给slf4j在真的需要输出时才去拼接的确能省节约成本。

但对于一定要输出的日志,直接自己用StringBuilder拼接更快。因为看看slf4j的实现,实际上就是不断的indexof(“{}”),
不断的subString(),再不断的用StringBuilder拼起来而已,没有银弹。

PS.
slf4j中的StringBuilder在原始Message之外预留了50个字符,如果可变参数加起来长过50字符还是得复制扩容……而且StringBuilder也没有重用。

4、StringBuilder和StringBuffer的“可变”特性总结如下:

8. 小结

StringBuilder默认的写法,会为129长度的字符串拼接,合共申请625字符的数组。所以高性能的场景下,永远要考虑用一个ThreadLocal
可重用的StringBuilder。而且重用之后,就不用再玩猜长度的游戏了。

(1)append,insert,delete方法最根本上都是调用System.arraycopy()这个方法来达到目的
(2)substring(int, int)方法是通过重新new String(value, start, end – start)的方式来达到目的。因此,在执行substring操作时,StringBuilder和String基本上没什么区别。

发表评论

电子邮件地址不会被公开。 必填项已用*标注