`

当心字符串连接的性能(关于String及StringBuilder的几点区别)

 
阅读更多

对于JAVA的字符串连接操作符(+)相信大家都十分熟悉 ,它的作用是把多个字符串合并为一个字符串,当然我们使用它是非常方便的 ,但它确不适合运用在大规模的场景中 。

 

下面我们通过程序说明一下:

假设我们有一个需求会对字符串进行数量很大连接操作

如果我们使用String进行操作,由于 String是不可变的,每次进行用(+)连接时都相当于重新创建了一个对象

这无疑是相当耗时的,以下程序进行了10万次的字符连接操作

 

[java] view plain copy
 
  1. public static void main(String[] args) {  
  2.         long start=System.currentTimeMillis();  
  3.         String string="aa";  
  4.         for (int i = 0; i <100000; i++) {  
  5.             string+="a";  
  6.         }  
  7.         long end=System.currentTimeMillis();  
  8.         System.out.println(end-start);  
  9.     }  


第一个程序输出8425(具体因计算机处理速度不定)

 

由于我们每次连接两个字符串时,它们的内容都要被拷贝 ,所以程序的执行时候是呈几何增长的。

为了获得可以接受的性能,我们可以用StringBuilder代替String 

 

[java] view plain copy
 
  1. public static void main(String[] args) {  
  2.     long start=System.currentTimeMillis();  
  3.     StringBuilder sb=new StringBuilder("aa");  
  4.     for (int i = 0; i <100000; i++) {  
  5.         sb.append("a");  
  6.     }  
  7.     long end=System.currentTimeMillis();  
  8.     System.out.println(end-start);  
  9. }  

第二个程序输出15(具体因计算机处理速度不定)

 

由程序的执行结果不难看出 ,二者的性能差十分的大,操作的数量越多 ,差距就越明显 。

 

结论:当我们的程序需要性能的时候,不要使用字符串连接操作符(+)来合并多个字符串 。最好使用StringBuilder的append方法。

 

 

关于String及StringBuilder的几点区别

直接看示例1:

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public class StringTest{  
  2.       
  3.     void stringReplace(String strTemp){  
  4.         strTemp=strTemp.replace('l','i');  
  5.     }  
  6.   
  7.     void stringBufferAppend(StringBuffer sbTemp){  
  8.         sbTemp=sbTemp.append('c');  
  9.     }  
  10.   
  11.     public static void main(String[] args){  
  12.         StringTest st=new StringTest();  
  13.         String str=new String("hello");  
  14.         StringBuffer sb=new StringBuffer("hello");  
  15.   
  16.         // String str1=new String("hello");  
  17.         // StringBuffer sb1=new StringBuffer("hello");  
  18.   
  19.         // System.out.println("str.equals(sb)= "+str.equals(sb));  
  20.         // System.out.println("str.equals(str1  )= "+str.equals(str1));  
  21.         // System.out.println("sb.equals(str)= "+sb.equals(str));  
  22.         // System.out.println("sb.equals(sb1)= "+sb.equals(sb1));  
  23.   
  24.         st.stringReplace(str);  
  25.         st.stringBufferAppend(sb);  
  26.         System.out.println("str= "+str+"\tsb= "+sb);  
  27.     }  
  28. }  

输出结果:

 

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. str= hello  sb= helloc  

上面的示例无非就是方法传值与传址的问题,因为形参为引用类型,因此实参传递过来的是对象的地址值,问题来了:改变该地址所对应的内容,实参应该也会跟着发生变化,如sb最终结果那样,但str却没有,为何?

 

这就是String与StringBuilder的区别:

字符串的可变与不可变

JDK_1.8中这样解释:

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. Strings are constant; their values cannot be changed after they are created.   
  2. String buffers support mutable strings. Because String objects are immutable   
  3. they can be shared.  

 

 

String创建的字符串是不可变的,而StringBuilder(或StringBuffer)通过字符缓冲区创建字符串,可变;

先来解析下String的不可变:

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. String s="hello";  
  2. s="world";  

对象s在创建的时候先查看常量池中是否有"hello",有则指向它,没有就创建一个"hello",再指向它,但当重新对s赋值为"world"时,常量池中的"hello"并没有改变,java会重新开辟一个内存存储新值"world",并令s指向"world";

因此在示例1中,当调用stringReplace()方法时,实参str所存储的地址值传给了形参strTemp后,strTemp指向了常量池中"hello",当对其进行字符替换时,java会重新创建"heiio",并让形参strTemp指向它,因此形参和实参指向了不同的区域,结果自然显而易见;

另外,String的不可变是由于:

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public final class String  
  2.     implements java.io.Serializable, Comparable<String>, CharSequence {  
  3.     private final char value[];//final的作用,使之初始化后不可改变  
  4.     ……  
  5. }  

 

(P.S.虽然value是用final来修饰的,但仍有办法可以直接改变其值,具体可以参见 反射 )

再来对比下StringBuilder的可变:

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. StringBuilder sb=new StringBuilder("hello");  
  2. sb.append("world");  

在创建StringBuilder对象时,java实际上在堆中创建了字符类型数组char[],存储完"hello"后通过append方法在增加"world",并未开辟新的内存区,形参和实参指向同一区域,因此形参对对象内容进行变化,实参也会跟着变化;

来看下StringBuilder构造器的底层代码:

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public StringBuilder() {  
  2.         super(16);  
  3.     }  
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public StringBuilder(int capacity) {  
  2.         super(capacity);  
  3.     }  
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public StringBuilder(String str) {  
  2.         super(str.length() + 16);  
  3.         append(str);  
  4. }  
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public StringBuilder(CharSequence seq) {  
  2.         this(seq.length() + 16);  
  3.         append(seq);  
  4. }  

其基类AbstractStringBuilder的构造方法:

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. char[] value;  
  2. AbstractStringBuilder(int capacity) {  
  3.         value = new char[capacity];  
  4. }  

……

 

可以发现StringBuilder实际创建了一个默认16字符长的char型数组(亦可指定长度):char[] value;

若为对象赋值时,所存储的字符串长度未超过所定义的char数组长度,则按顺序存储相应字符,若超过所定义的数组长度,则自动扩充其长度,因此说StringBuffer是可变的;

我们还是继续看append()底层代码比较形象:

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public AbstractStringBuilder append(String str) {  
  2.         if (str == null)  
  3.             return appendNull();  
  4.         int len = str.length();  
  5.         ensureCapacityInternal(count + len);  
  6.         str.getChars(0, len, value, count);  
  7.         count += len;  
  8.         return this;  
  9. }  
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. private void ensureCapacityInternal(int minimumCapacity) {  
  2.         if (minimumCapacity - value.length > 0)  
  3.             expandCapacity(minimumCapacity);  
  4. }  
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. void expandCapacity(int minimumCapacity) {  
  2.         int newCapacity = value.length * 2 + 2;  
  3.         if (newCapacity - minimumCapacity < 0)  
  4.             newCapacity = minimumCapacity;  
  5.         if (newCapacity < 0) {  
  6.             if (minimumCapacity < 0// overflow  
  7.                 throw new OutOfMemoryError();  
  8.             newCapacity = Integer.MAX_VALUE;  
  9.         }  
  10.         value = Arrays.copyOf(value, newCapacity);  
  11. }  

当为StringBuilder对象增加内容时,会先计算所需最小空间:minimumCapacity=count+len;并与原数组长度的2倍+2即newCapacity进行比较,取较大值并为数组value重新赋值,赋值采用的是数组复制的方式进行;

常见的数组复制有两种方法:一为:Arrays.copyOf(),另一为:System.arraycopy(),差别在于后者可能存在下标越界的问题,其实前者也是通过调用后者的方法来实现的:

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public static char[] copyOf(char[] original, int newLength) {  
  2.         char[] copy = new char[newLength];  
  3.         System.arraycopy(original, 0, copy, 0,  
  4.                          Math.min(original.length, newLength));  
  5.         return copy;  
  6. }  

由于重新为临时数组copy定义了长度newLength,因此复制的时候不会出现越界问题;在数组复制结束后,将临时数组copy赋值给value,使之指向新的区域,完成append()操作 (这里注意:引用变量value的指向发生了变化,但StirngBuilder的指向没有变,只不过其成员变量(恰巧是个引用变量value)的值发生了改变,因此形参sbTemp和sb仍指向同一区域);

equals方法

示例1中注释掉的几个equals()语句,结果只有String与String比较时才返回true,这是String与StringBuilder的另一个区别;

我们要直到equals()方法的返回结果取决于你如何重写的,如直接继承Object的方法,则比较的是变量的值,对于StringBuilder,它并未重写该方法,因此对两个引用变量进行比较自然返回false;而String类重写了该方法,具体如下:

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public boolean equals(Object anObject) {  
  2.         if (this == anObject) {  
  3.             return true;  
  4.         }  
  5.         if (anObject instanceof String) {  
  6.             String anotherString = (String)anObject;  
  7.             int n = value.length;  
  8.             if (n == anotherString.value.length) {  
  9.                 char v1[] = value;  
  10.                 char v2[] = anotherString.value;  
  11.                 int i = 0;  
  12.                 while (n-- != 0) {  
  13.                     if (v1[i] != v2[i])  
  14.                         return false;  
  15.                     i++;  
  16.                 }  
  17.                 return true;  
  18.             }  
  19.         }  
  20.         return false;  
  21.     }  

可知,若两个引用变量指向同一区域,则自然返回true,若非,则判断anObject是否是String的实例,若是则进行值比较,否则直接返回false;

 

 

效率的差别

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. public class StringBuilderTest2{  
  2.     public static void main(String[] args){  
  3.               
  4.             StringBuilder sb=new StringBuilder();  
  5.             StringBuilder sb1=new StringBuilder(Integer.MAX_VALUE/7);  
  6.             String s=null;  
  7.             System.out.println(Integer.MAX_VALUE+"\n"+sb1.capacity());  
  8.             long start=System.currentTimeMillis(),end;  
  9.             for(int i=0;i<1000000;i++){  
  10.                 sb.append(1);  
  11.             }  
  12.             end=System.currentTimeMillis();  
  13.             System.out.println("默认初始化SB时循环100w次耗时:"+(end-start));  
  14.             for(int i=0;i<1000000;i++){  
  15.                 sb1.append(1);  
  16.             }  
  17.             start=System.currentTimeMillis();  
  18.             System.out.println("给定值初始化SB1时循环100w次耗时:"+(start-end));  
  19.               
  20.             for(int i=0;i<100000;i++){  
  21.                 s+=1;  
  22.             }  
  23.             end=System.currentTimeMillis();  
  24.             System.out.println("String“+”运算10W次耗时:"+(end-start));  
  25.         }  
  26. }  

输出:

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. 2147483647  
  2. 306783378//初始化值  
  3. 默认初始化SB时循环100w次耗时:35  
  4. 给定值初始化SB1时循环100w次耗时:16  
  5. String“+”运算10W次耗时:3211  

可以发现在循环“+”/append()运算时,String类的效率明显低于StringBuilder,为何?

 

这里继续使用javap来分析:

 

[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. String s=null;  
  2. s+=1;  
[java] view plain copy
 
 在CODE上查看代码片派生到我的代码片
  1. 0: aconst_null     
  2. 1: astore_1        
  3. 2new           #2                  // class java/lang/StringBuilder  
  4. 5: dup             
  5. 6: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V  
  6. 9: aload_1         
  7. 10: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;  
  8. 13: iconst_1        
  9. 14: invokevirtual #5                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;  
  10. 17: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;  
  11. 20: astore_1        
  12. 21return   


可以看到编译器在解析String"+"运算时,会转换成StringBuilder,调用append()方法,再通过toString()赋值,因此循环运算时,就不断发生创建对象和产生垃圾,这过程消耗了资源,造成了效率的下降;

 

另外,我们也可以发现调用StringBuilder构造方法时若给它传入较大整数作为参数,则运算效率也会有明显的提升,这跟前面所提扩容时数组复制有关,给定较大初始缓存区,自然不需要频繁扩容;
当然,这里顺带提一下,如果直接把String的"+"运算写成形如:s=""+1+1+1……,而不是通过循环来不断调用变量,则编译器会直接求出其字面量,运行时不会再转换为StringBuilder,效率当然就比后者高了;

 

分享到:
评论

相关推荐

    字符串、String类和StringBuilder类

    本文将深入探讨字符串、String类以及StringBuilder类,帮助你更好地理解和应用这些基础知识。 首先,我们关注的是String类。在C#中,String类是不可变的,这意味着一旦一个字符串对象被创建,它的内容就不能被改变...

    String和StringBuilder类的区别

    - **StringBuilder**:由于 `StringBuilder` 允许在原有对象上进行修改,因此在进行字符串拼接等操作时,其性能通常优于 `String`。特别是当字符串拼接操作非常频繁时,这种性能优势尤为明显。 #### 3. 使用场景 -...

    String、StringBuilder和StringBuffer的区别

    在Java编程语言中,String、StringBuilder和StringBuffer都是用来处理字符串的类,它们之间存在一些重要的区别,主要涉及到性能和线程安全性。 首先,`String`类代表的是字符串常量,一旦创建,其内容就不能改变。...

    String StringBuffer和StringBuilder区别之源码解析

    "String StringBuffer和StringBuilder区别之源码解析" 在Java中,字符串是我们经常使用的数据类型,而String、StringBuffer和StringBuilder是Java中三种常用的字符串类。在这篇文章中,我们将从源码角度对String、...

    字符串数组转换成string类型的

    在编程领域,尤其是在使用C++、Java或C#等面向对象的语言时,经常需要将字符串数组转换为单一的string类型。这种操作在处理数据输入、输出或者格式化时非常常见。下面我们将详细讨论如何在不同语言中实现这个过程,...

    测试string的+和String.Format()和StringBuilder.Append()性能差距|TestForString.7z

    本测试着重探讨了三种常用的字符串连接方法:`+`运算符、`String.Format()`以及`StringBuilder.Append()`,并分析了它们在性能上的差异。 1. **字符串连接:+ 运算符** 在C#中,`+`运算符可以用于连接两个或多个...

    StringBuilder字符串生成工具

    主要生成StringBuilder 字符串 类似 StringBuilder builder = new StringBuilder(); builder.AppendFormat("&lt;span class=\"navSep\"&gt;&lt;/span&gt;\r\n"); builder.AppendFormat("机构看盘&lt;/a&gt;\r\n"); builder....

    String及StringBuffer和StringBuilder的区别

    String、StringBuffer 和 StringBuilder 是 Java 语言中三种不同类型的字符串处理方式,它们之间存在着明显的性能和线程安全性差异。 String String 类型是不可变的对象,每次对 String 对象进行改变时都会生成一...

    C# 拼接字符串的几种方式和性能

    C# 拼接字符串的几种方式和性能 C# 拼接字符串的方式有多种,每种方式都有其优缺,今天我们将讨论三种常用的方式:简单“+=”拼接法、String.Format()和StringBuilder.Append()。 1. 简单“+=”拼接法 简单“+=”...

    C#字符串删除指定字符串|字符串删除子字符串

    在C#编程语言中,处理字符串是常见的任务之一,其中包括删除字符串中的特定部分或子字符串。本篇文章将详细探讨如何在C#中实现这一功能,包括多种方法和实用技巧。 首先,C#提供了多种内置方法来操作字符串,比如`...

    StringBuilder字符串拼接工具

    在处理大量字符串连接时,相比直接使用`+`运算符或`String`对象,`StringBuilder`能提供更高的性能。这是因为每次使用`+`进行字符串连接时,Java都会创建新的`String`对象,这在内存管理和效率上都是不理想的。而`...

    StringBuilder拼接字符串

    C# StringBuilder 拼接字符串 字符串转换工具 StringBuilder比StringBuffer运行速度要快,因为StringBuilder是针对于单线程的,所这它是非线程安全的。普通情况下建议使用StringBuilder。

    String和StringBuilder效率测试

    在Java编程语言中,`String`和`StringBuilder`都是用于处理字符串的重要类,但它们在处理方式和效率上有显著的区别。本资源中的代码是针对这两种类的效率进行测试的实例,旨在帮助开发者理解它们在不同场景下的性能...

    C#字符串连接方法性能比较器代码

    C#中有多种连接字符串的方法,例如+、$、StringBuilder、Concat等,这里对前三种方法的性能进行了粗略比较,结果表明在字符串较小、使用频率很低的场合,使用“+”或"$"都行,如果连接的数据类型都是字符串,则推荐...

    String、StringBuffer、StringBuilder的使用方法

    在Java编程语言中,`String`、`StringBuffer`和`StringBuilder`是处理字符串的三个重要类,它们各自有特定的使用场景和优缺点。理解它们的差异对于编写高效的代码至关重要。 **String类** `String`是不可变的类,...

    java通用字符串连接

    在这个例子中,`join()`方法使用了`StringBuilder`来避免创建大量的中间字符串对象,从而提高了性能。`join(Iterable, String)`方法适用于任何实现了`Iterable`接口的集合,包括List、Set等。`join(Map, ?&gt;, String)...

    关于js中连接字符串效率的测试例子!

    在内部,JavaScript会创建新的字符串对象来存储连接结果,因此这种方式对于少量字符串连接效率尚可,但随着字符串数量增加,性能会下降,因为每次连接都会产生新的字符串对象。 2. 使用`Array.prototype.join()`:...

    计算机科学与技术-外文翻译-外文文献-英文文献-提高字符串处理性能的应用程序.doc

    StringBuilder 方法可以将多个字符串连接起来生成最终的字符串,具有高效的性能和良好的可扩展性。 2. 使用内置方法 内置方法可以将多个字符串连接起来生成最终的字符串,具有高效的性能和良好的可扩展性。 3. ...

    String ,StringBuffer与StringBuilder

    在 Java 中,String, StringBuffer 和 StringBuilder 三个类都是用于字符操作的,但它们之间有着很大的区别。 首先,String 是不可变类,意味着一旦创建了 String 对象,就不能修改它的值。每次对 String 对象的...

Global site tag (gtag.js) - Google Analytics