java字符串连接

由于String是不可变对象(final),所以,对字符串进行连接、替换操作时,String对象总是会生成新的对象。所以连接和替换时性能很差。

String常量字符串的累加

比如我们使用如下代码进行字符串连接:

String str = "hello"+"world"+"!";

先有hello和world2个字符串生成helloworld,然后再生成helloworld!。

将上面的代码做5万次循环。
但上面的代码执行效率竟然比使用StringBuilder快,为什么呢?

StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append("world");
sb.append("!");

对第一段代码进行反编译,可以看到对于常量字符串的累加,java在编译时就做了优化。

String str = "helloworld!";

String变量字符串的累加

public class Test {
    public static void main(String[] args) {
        String a = "hello";
        String b = "world";
        String c = "!";
        int loopCount = 50000000;
        long s = System.currentTimeMillis();
        for (int i=0;i//            test(a,b,c);
            test2(a,b,c);
        }
        System.out.println("cost " + (System.currentTimeMillis() - s) + "ms.");
    }
    private static void test(String a,String b,String c) {
        StringBuilder stringBuilder = new StringBuilder();
        String s = stringBuilder.append(a).append(b).append(c).toString();
    }
    private static void test2(String a,String b,String c) {
        String s = a + b + c;
    }
}

同样做5万次循环,发现与使用StringBuilder性能差不多。
反编译,发现java在编译时做了优化。

public class Test {
    public Test() {
    }
    public static void main(String[] args) {
        String a = "hello";
        String b = "world";
        String c = "!";
        int loopCount = 50000000;
        long s = System.currentTimeMillis();
        for(int i = 0; i < loopCount; ++i) {
            test2(a, b, c);
        }
        System.out.println("cost " + (System.currentTimeMillis() - s) + "ms.");
    }
    private static void test(String a, String b, String c) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(a).append(b).append(c).toString();
    }
    private static void test2(String a, String b, String c) {
        (new StringBuilder()).append(a).append(b).append(c).toString();
    }
}

可以看到,java在编译的时候将字符串的+操作转换成了使用StringBuilder的append方式。

构建超大的字符串

对下面的代码ABC分别执行10000次。
代码A:

String s = "";
for (int i = 0; i < 1000; i++) {
    s = s + i;
}

执行耗时:4542ms。

代码B:

String s = "";
for (int i = 0; i < 1000; i++) {
    s = s.concat(String.valueOf(i));
}

执行耗时:4079ms。

代码C:

StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    stringBuilder.append(i);
}
String s = stringBuilder.toString();

执行耗时:259ms。

可以看到,从快到慢依次是StringBuilder > String.concat() > String+。并且StringBuilder要快很多。

观察编译后的代码发现代码Ajava编译器并没有做任何优化。

选择StringBuilder还是StringBuffer?

StringBuffer与StringBuilder最大的不同在于,StringBuffer对几乎所有的方法都做了同步,StringBuilder没有做任何同步。
由于方法同步需要消耗一定的系统资源,因此,StringBuffer效率要低于StringBuilder。但是,在多线程环境中,StringBuilder无法保证线程安全,不能使用。

所以,如果是不需要考虑线程安全的情况下,使用StringBuilder;相反则使用StringBuffer。

容量参数

无论是StringBuffer还是StringBuilder都可以在初始化时设置一个容量参数。

public StringBuffer(int capacity);
public StringBuilder(int capacity);

在不指定容量参数时,默认是16个字符。
代码如下:

public StringBuilder() {
    super(16);
}

StringBuffer和StringBuilder都继承自AbstractStringBuilder,AbstractStringBuilder的构造器代码:

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

不带容量参数测试

//StringBuilder sb = new StringBuilder();
StringBuffer sb = new StringBuffer();
for (int i=0;i<10000000;i++) {
    sb.append(i);
}

StringBuilder耗时405ms,StringBuffer耗时557ms。

带容量参数测试

//StringBuilder sb = new StringBuilder(68888890);
StringBuffer sb = new StringBuffer(68888890);
for (int i=0;i<10000000;i++) {
    sb.append(i);
}
//System.out.println(sb.length());

StringBuilder耗时330ms,StringBuffer耗时464ms。

通过对比,可以看到增加容量参数可以增加StringBuffer和StringBuilder的性能。

StringBuffer和StringBuilder在执行append方法时,实际是调用父类AbstractStringBuilder的append方法。
父类append方法定义如下:

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

可以看到一个ensureCapacityInternal方法,定义如下:

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

minimumCapacity就是现在存储的数据的长度+本次append字符串的长度。如果该长度比定义的保存数据的char[]的长度大,说明char[]存储空间不够,需要进行扩容了。

扩容的逻辑:新的容量为目前数据的容量的2倍+2;如果扩容后的长度仍然小于目前数据的长度+本次append字符串的长度,则新的容量为目前数据的长度+本次append字符串的长度。
然后执行了一次数组的复制,将旧的数据复制到新的数组中。

所以,如果指定合适的容量,可以避免SringBuilder和StringBuffer的内存复制,这样可以提升append的性能。
这个跟HashMap比较像,HashMap在put数据时,也会在容量不够时进行扩容。

参考:《Java程序性能优化——让你的java程序更快、更稳定》

你可能感兴趣的:(java字符串连接)