java中,字符串拼接方式与效率分析
由于String对象时不可变对象,因此在需要对字符串进行修改操作时(如字符串连接和替换),String对象总是会生成新的对象,所以其性能相对较差。
String常量的累加操作:对于静态字符串的连接操作,Java在编译时会进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。
String变量的累加操作:底层使用了StringBuilder的功能。
StringBuffer和StringBuilder的扩容策略:当字符串缓冲区容量不足时,原有容量将会加倍,以新的容量来申请内存空间,建立新的char数组,然后将原数组中的内容复制到这个新的数组当中。因此,对于大对象的扩容会涉及大量的内存复制操作。所以,如果能够预先评估StringBuilder或StringBuffer的大小,将能够有效的节省这些操作,从而提高系统的性能。
JAVA的字符串拼接与性能
在JAVA中拼接两个字符串的最简便的方式就是使用操作符”+”了。如果你用”+”来连接固定长度的字符串,可能性能上会稍受影响,但是如果你是在循环中来”+”多个串的话,性能将指数倍的下降。假设有一个字符串,我们将对这个字符串做大量循环拼接操作,使用”+”的话将得到最低的性能。但是究竟这个性能有多差?
+, Join,StringBuffer,StringBuilder或String.concat()
1.String 是final对象,不会被修改,每次使用 + 进行拼接都会创建新的对象,而不是改变原来的对象,也属于线程安全的;
2.StringBuffer可变字符串,主要用于字符串的拼接,属于线程安全的;(StringBuffer的append操作用了synchronized)
3.StringBuilder可变字符串,主要用于字符串的拼接,属于线程不安全的;
(1)循环操作:
import java.util.ArrayList; import java.util.List; import org.apache.commons.lang3.StringUtils; public class TestString { public static void main(String[] args) { //plus拼接字符串方式 String s = ""; long ts = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { s = s + String.valueOf(i); } long te = System.currentTimeMillis(); System.out.println("Plus cost {"+( te - ts) +"} ms"); //concat拼接字符串方式 String s2 = ""; long ts2 = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { s2 = s2.concat(String.valueOf(i)); } long te2 = System.currentTimeMillis(); System.out.println("concat cost {"+(te2 - ts2)+"} ms"); //StringUtils.join拼接字符串方式 List<String> list = new ArrayList<String>(); long ts3 = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { list.add(String.valueOf(i)); } StringUtils.join(list, ""); long te3 = System.currentTimeMillis(); System.out.println("StringUtils.join cost {"+(te3 - ts3)+"} ms"); //StringBuffer拼接字符串方式 StringBuffer sb = new StringBuffer(); long ts4 = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { sb.append(String.valueOf(i)); } sb.toString(); long te4 = System.currentTimeMillis(); System.out.println("StringBuffer cost {"+(te4 - ts4)+"} ms"); //StringBuilder拼接字符串方式 StringBuilder sb5 = new StringBuilder(); long ts5 = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { sb5.append(String.valueOf(i)); } sb5.toString(); long te5 = System.currentTimeMillis(); System.out.println("StringBuilder cost {"+(te5 - ts5)+"} ms"); } }
要特别注意的是:
StringBuilder和StringBuffer循环的次数是其它的10倍,如果是一样,那么可能返回0,可见StringBuilder和StringBuffer的速度之快。)
系统:(四核cup)
结果:(10倍)
Plus cost {229} ms concat cost {116} ms StringUtils.join cost {22} ms StringBuffer cost {8} ms StringBuilder cost {7} ms
正常情况下:
plus cost {249} ms concat cost {115} ms StringUtils.join cost {20} ms StringBuffer cost {1} ms StringBuilder cost {1} ms
结果分析:
(1)StringBuilder性能比StringBuffer要好点。在1千万的循环下, StringBuilder大约在500-600毫秒,而StringBuffer大约在700-800毫秒;
(2) StringUtils.join方式性能也是可以的,在循环不是很大的情况下,该种方式也是可以考虑的;
(3)另外2种方式,在循环的下,尽量不要使用。
因此,即时在做最简单的拼接时,如果我们不想创建StringBuffer或StringBuilder实例使,我们也因该使用concat。但是对于大量的字符串拼接操作,就不应该使用concat,因为concat会降低你程序的性能,消耗你的cpu。因此,在不考虑线程安全和同步的情况下,为了获得最高的性能,我们应尽量使用StringBuilder。
总结:
1.用+的方式效率最差,concat由于是内部机制实现,比+的方式好了不少。
2.Join和StringBuffer,相差不大,Join方式要快些,可见这种JavaScript中快速拼接字符串的方式在Java中也非常适用。
3.StringBuilder 的速度最快,但其有线程安全的问题,而且只有JDK5及以上的版本支持。
4.String对象串联的效率最慢,单线程下字符串的串联用StringBuilder,多线程下字符串的串联用StrngBuffer;
5.在编译阶段就能够确定的字符串常量,完全没有必要创建String或StringBuffer对象。直接使用字符串常量的"+"连接操作效率最高(如:String str = “a” + “b” + “c”;)。
源码:
1、其实每次调用contact()方法就是一次数组的拷贝,虽然在内存中是处理都是原子性操作,速度非常快,但是,最后的return语句会创建一个新String对象,限制了concat方法的速度。
public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); }
2. StringBuffer和StringBuilder的append方法都继承自AbstractStringBuilder,整个逻辑都只做字符数组的加长,拷贝,到最后也不会创建新的String对象,所以速度很快,完成拼接处理后在程序中用strBuffer.toString()来得到最终的字符串。
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; } 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); }
3. 字符串的加号“+” 方法, 虽然编译器对其做了优化,使用StringBuilder的append方法进行追加,但是每循环一次都会创建一个StringBuilder对象,且都会调用toString方法转换成字符串,所以开销很大(时间、空间)。
如JVM对于“+”是这样处理的,首先创建一个String对象str,并把“abc”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“de”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了,所以,str实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。
注:执行一次字符串“+”,相当于 str = new StringBuilder(str).append("a").toString()。
大家好,我是威5,更多精彩关注wx公众号: