从源码的角度解读String、StringBuilder、StringBuffer的性能差别

首先我们来看一段程序

package more;
/**
 * 测试string StringBuffer StringBuilder的性能
 * @author ybsem
 *
 */
public class SSbSb {
    public static void main(String[] args) {
        String string = "s";
        StringBuilder stringBuilder = new StringBuilder("s");
        StringBuffer stringBuffer = new StringBuffer("s");
        
        Long startString = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            string = string + i;
        }
        Long endString = System.currentTimeMillis();
        System.out.println("创建string常量时间为:"+(endString-startString));
        
        Long startStringBuilder = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            stringBuilder = stringBuilder.append(i);
        }
        Long endStringBuilder =System.currentTimeMillis();
        System.out.println("创建stringBuilder对象时间为:"+(endStringBuilder-startStringBuilder));
        
        Long startStringBuffer = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            stringBuffer = stringBuffer.append(i);
        }
        Long endStringBuffer = System.currentTimeMillis();
        System.out.println("创建stringBuffer对象时间为:"+(endStringBuffer-startStringBuffer));
        
    }
}

 

测试结果

由此我们可以从性能上看出String<StringBuffer<StringBuilder

现在我们从源码的角度来解读下为什么出现这种问题

String


 首先来看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 string = “s”其实是一个字符数组,并且是不可变的10000; i++) { string = string + i; }

这段代码其实相当于创建了一万个新的String对象,所以有大量对字符串的操作时不宜使用String

StringBuilder

 我们来看下StringBuilder的核心源码

@Override
    public StringBuilder append(Object obj) {
        return append(String.valueOf(obj));  //当传入的是一个对象时先将其转换为String类型
    }

    @Override
    public StringBuilder append(String str) {
        super.append(str);  //执行父类的append()方法,他的父类为AbstractStringBuilder

        return this;
    }

 

//StringBuilder的父类append()方法public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);  //扩容函数
        str.getChars(0, len, value, count);     //value是原字符串的值
/*
 * public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
 *      if (srcBegin < 0) {
 *           throw new StringIndexOutOfBoundsException(srcBegin);
 *       }
 *       if (srcEnd > value.length) {
 *           throw new StringIndexOutOfBoundsException(srcEnd);
 *      }
 *      if (srcBegin > srcEnd) {
 *          throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
 *      }
 *      System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin); //是一个本地        //方法用将两个字符串相连,通过数组的copy
 * }
*
*/
       count += len;   //conut初始值为0,第一次执行append()方法时count变为字符        //串的值
        return this;
    }

由上边的源码可以看出StringBuilder是通过底层数组进行连接的效率高,但同时也应该看到该段代码没有进行同步,所以在多线程环境下是不安全的

StringBuffer


再来看StringBuffer的源码

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

 /父类的appen()的方法

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

//源码与StringBuilder基本相同

StringBuffer与StringBuilder的区别在于将append()进行了同步,保证了多线程条件下的安全性,但在获得安全性的同时牺牲了性能,所以StringBuffer要比StringBuilder要快的原因

总结如下:

  • 在进行少量对字符串的操作时使用String
  • 在多线程环境下,并且对字符串操作次数较多则使用StringBuffer
  • 在单线程环境下,并且对字符串操作次数较多则使用StringBuilde

分析完性能我们再来详细的分析下append()方法


先来分析String的构造函数

/** The value is used for character storage. */
    private final char value[];

     /* Initializes a newly created {@code String} object so that it represents
     * the same sequence of characters as the argument; in other words, the
     * newly created string is a copy of the argument string. Unless an
     * explicit copy of {@code original} is needed, use of this constructor is
     * unnecessary since Strings are immutable.
     */
public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

有这段代码可知以及代码注释可知,string其实是一个数组,由于他是一个不可变类,新创建的字符串是参数字符串的副本。 所以呢除非需要这个副本,否则没必要用这个构造函数来进行创造。String类的字符串串创建时数组的大小是由字符串常量池也就是jvm来进行分配的。具体我们可以来分析下StringBuilder

先来看一段stringBuilder的构造函数

 

 1    /**
 2      * Constructs a string builder with no characters in it and an
 3      * initial capacity of 16 characters.
 4      */
 5     public StringBuilder() {
 6         super(16);
 7     }
 8    /**
 9      * Constructs a string builder with no characters in it and an
10      * initial capacity specified by the {@code capacity} argument.
11      */
12  public StringBuilder(int capacity) {
13         super(capacity);
14     }
15 
16     /**
17      * Constructs a string builder initialized to the contents of the
18      * specified string. The initial capacity of the string builder is
19      * {@code 16} plus the length of the string argument.
20      */
21    public StringBuilder(String str) {
22         super(str.length() + 16);
23         append(str);
24     }

 

 /**上述构造函数调用super函数
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

 

这段代码清晰的看出他是基于数组构建的。初始大小为16,stringBuilder继承子AbstractStringBuilder。其次需要了解下他的一个很用要的方法append()方法,它是被用于字符串的连接。可以看出在连接的第一步就需要检查数组大小是否足够(不检查若连接长点的字符串必然会发生数组越界),

 

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

 

 

 

    public void ensureCapacity(int minimumCapacity) {
        if (minimumCapacity > 0)
            ensureCapacityInternal(minimumCapacity);
    }
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }

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

 

检查数组大小是否足够分三步,count+len其实就是新字符床的长度(count旧字符串的长度len需要连接字符串的额长度),若最小长度也就是count+len小于0,不做任何事返回,因为这代表没有任何意义。若大于零则执行ensureCapacityInternal方法,value.length代表的是连接前的字符串的值,若新字符串也就是连接后字符串的大小大于旧字符串的长度,则代表数组大小可能不够用,进行扩容。执行expandCapacity方法,扩容后大小为length*2+2这是很可怕的,如果在构造的时候没有指定容量,那么很有可能在扩容之后占用了浪费大量的内存空间。使用StringBuilder或者StringBuffer时,尽可能准确地估算capacity,并在构造时指定,避免内存浪费和频繁的扩容及复制。扩容之后新申请的数组大小newCapacity小于0同时连接后字符串所需最小空间minimumCapacity也小于0,代表无法申请到足够大的空间,发生内存溢出抛出异常。若申请到改大小的空间之后执行数组的复制,返回连接后的数组,相当于新建了一个字符串的值。

StringBuffer进行复制的原理基本相同。

字符串连接时应该注意

 


若用String类型的字符串通过+进行拼接时,会创建一个新的String对象,你要知道String对象一旦创建就是不能被改变的,要达到字符串拼接的效果,就得不停创建新对象。StringBuilder直到最后sb.toString()才会创建String对象,之前都没有创建新对象,或者在超过数组代表长度的字符串时才进行字符串的创建,故从效率上来讲append()要优于+

 

posted @ 2017-02-23 13:42  短腿的兔子跑得快  阅读(230)  评论(0编辑  收藏  举报