当心字符串连接的性能(关于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,效率当然就比后者高了;

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值