(三)String、StringBuilder、StringBuffer在字符串操作中的性能差异浅析
参考资料:https://www.iteye.com/blog/hank4ever-581463
《Core Java Volume I-Fundamentals》原书第十版
《Java编程思想》原书第四版
一、初识String
Java字符串是由char值的序列组成,而char数据类型是一个采用UTF-16编码(一个char数据类型的数据大小为16bit)表示Unicode码点的代码单元。大多数常用Unicode字符使用一个代码单元(两个字节)就可以表示,而一些辅助字符用两个代码单元(四个字节)表示。
注:在编译的时候,如果我们没有用-encoding参数指定我们的JAVA源程序的编码格式,则javac.exe首先获得我们操作系统默认采用的编码格式,也即在编译java程序时,若我们不指定源程序文件的编码格式,JDK首先获得操作系统的file.encoding参数(它保存的就是操作系统默认的编码格式,如WIN10,它的默认编码方式为GBK),然后JDK就把我们的java源程序从file.encoding编码格式转化为JAVA内部默认的UNICODE格式放入内存中。
可以使用使用-encoding参数指明JAVA编译时的编码方式:javac -encoding UTF-8 XX.java,如果使用IDEA等开发工具也可以直接设置IDEA开发的JAVA文件其默认编码方式。
代码示例:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public static String getByteHexadecimal(byte [] btArr){ 2 char [] Hexadecimal = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; 3 StringBuilder builder=new StringBuilder(); 4 for(int i=0;i<btArr.length;i++){ 5 builder.append(Hexadecimal[btArr[i]>>4 & 0XF]); 6 builder.append(Hexadecimal[btArr[i] & 0XF]); 7 } 8 return builder.toString(); 9 } 10 public static void main(String[] args) { 11 String s1="a"; 12 String s2="字"; 13 String s3="𝄐"; 14 15 try { 16 //得到当前的默认编码 17 String csn = Charset.defaultCharset().name(); 18 System.out.println("IDEA设置默认编码方式为UTF-8;现在JAVA文件的编码方式为: "+csn); 19 20 //分别测试一个字母、一个中文和一个特殊字符占用的内存大小 21 byte[]cr1=s1.getBytes(); 22 System.out.println(csn+ " 编码下 a 占用的字节数)为: "+cr1.length+"byte"+" 16进制表示为"+getByteHexadecimal(cr1)); 23 byte[]cr2=s2.getBytes(); 24 System.out.println(csn+ " 编码下 字 占用的字节数为: "+cr2.length+"byte"+" 16进制表示为"+getByteHexadecimal(cr2)); 25 byte[]cr3=s3.getBytes(); 26 System.out.println(csn+ " 编码下 \uD834\uDD10 占用的字节数为: "+cr3.length+"byte"+" 16进制表示为"+getByteHexadecimal(cr3)); 27 System.out.println(""); 28 29 30 //测试UTF-16编码 31 csn ="UTF-16"; 32 //分别测试一个字母、一个中文和一个特殊字符占用的内存大小 33 cr1=s1.getBytes(csn); 34 System.out.println(csn+ " 编码下 a 占用的字节数)为: "+cr1.length+"byte"+" 16进制表示为"+getByteHexadecimal(cr1)); 35 cr2=s2.getBytes(csn); 36 System.out.println(csn+ " 编码下 字 占用的字节数为: "+cr2.length+"byte"+" 16进制表示为"+getByteHexadecimal(cr2)); 37 cr3=s3.getBytes(csn); 38 System.out.println(csn+ " 编码下 \uD834\uDD10 占用的字节数为: "+cr3.length+"byte"+" 16进制表示为"+getByteHexadecimal(cr3)); 39 System.out.println(""); 40 41 //测试GBK编码 42 csn ="GBK"; 43 //分别测试一个字母、一个中文和一个特殊字符占用的内存大小 44 cr1=s1.getBytes(csn); 45 System.out.println(csn+ " 编码下 a 占用的字节数)为: "+cr1.length+"byte"+" 16进制表示为"+getByteHexadecimal(cr1)); 46 cr2=s2.getBytes(csn); 47 System.out.println(csn+ " 编码下 字 占用的字节数为: "+cr2.length+"byte"+" 16进制表示为"+getByteHexadecimal(cr2)); 48 cr3=s3.getBytes(csn); 49 System.out.println(csn+ " 编码下 \uD834\uDD10 占用的字节数为: "+cr3.length+"byte"+" 16进制表示为"+getByteHexadecimal(cr3)); 50 System.out.println(""); 51 52 }catch (Exception e){ 53 54 } 55 }
运行结果:
注:UTF-16编码下前缀的FEFF是用来标记字节顺序的。比如说对于UTF-16,如果前缀是FEFF,表明这个字节流是Big-Endian的;如果前缀是FFFE,就表明这个字节流是Little-Endian的。所以字节数比实际还要增加两个字节。
二、String对象
1、String对象是不可变的,用String类中的方法在对String进行修改时,基本都是创建了一个新的String对象,而最初的String对象并未改变。以下来测试一下数个字符串用 + 号拼接。
代码示例(实验环境:JDK8):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class test2 { 2 public static void main(String[] args) { 3 String str1="在中间"; 4 String str2="我"+str1+"要"+"拼"+"一"+"个"+"字"+"符"+"串"; 5 System.out.println(str2); 6 } 7 }
用 javac -encoding UTF-8 test2.java 将代码编译成字节码文件,此处加 -encoding UTF-8 确保编码使用Unicode编码而不是系统默认的GBK编码,以免产生乱码
用 javap -c test2.class 反编译字节码文件得到"虚拟机的汇编代码"
Compiled from "test2.java" public class JAVASE_Fundamentals.chapter3.test2 { public JAVASE_Fundamentals.chapter3.test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String 在中间 2: astore_1 3: new #3 // class java/lang/StringBuilder 6: dup 7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 10: ldc #5 // String 我 12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: aload_1 16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 19: ldc #7 // String 要拼一个字符串 21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 27: astore_2 28: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 31: aload_2 32: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 35: return }
以上可以看出编译后的拼接过程中自动初始化了一个StringBuilder对象,并使用StringBuilder的append()方法对字符串进行拼接,编译器对代码进行了优化
所以如果字符串对象的拼接过程不参与多次循环重复时,可以直接使用 + 或者 += 来拼接字符串对象,编译后实际上会初始化一个StringBuilder对象来进行拼接。但是如果字符串多次循环进行拼接时,每次循环拼接都将会初始化一个StringBuilder对象,为了避免初始化多个Stringbuilder对象的资源浪费,提高程序效率,此时应该直接在循环外创建一个StringBuilder对象,并使用这个对象的append()方法来进行拼接。
例如一个空串从0一直循环拼接到50000,使用 + 来拼接
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public static void main(String[] args) { 2 Long dateStart=System.currentTimeMillis(); 3 String str=""; 4 for (int i=0;i<50000;i++){ 5 str+=i; 6 } 7 Long dateEnd=System.currentTimeMillis(); 8 System.out.println("使用 +拼接字符串 从0拼接到50000 用时: "+(dateEnd-dateStart)+"毫秒"); 9 }
执行结果: 使用 +拼接字符串 从0拼接到50000 用时: 6320毫秒
反编译代码:
Compiled from "test3.java" public class JAVASE_Fundamentals.chapter3.test3 { public JAVASE_Fundamentals.chapter3.test3(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J 3: invokestatic #3 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long; 6: astore_1 7: ldc #4 // String 9: astore_2 10: iconst_0 11: istore_3 12: iload_3 13: ldc #5 // int 50000 15: if_icmpge 43 18: new #6 // class java/lang/StringBuilder 21: dup 22: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V 25: aload_2 26: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 29: iload_3 30: invokevirtual #9 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 33: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 36: astore_2 37: iinc 3, 1 40: goto 12 43: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J 46: invokestatic #3 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long; 49: astore_3 50: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; 53: new #6 // class java/lang/StringBuilder 56: dup 57: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V 60: ldc #12 // String 使用 +拼接字符串 从0拼接到50000 用时: 62: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 65: aload_3 66: invokevirtual #13 // Method java/lang/Long.longValue:()J 69: aload_1 70: invokevirtual #13 // Method java/lang/Long.longValue:()J 73: lsub 74: invokevirtual #14 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder; 77: ldc #15 // String 毫秒 79: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 82: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 85: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 88: return }
可以发现15行到40行构成了一个循环体,每次循环都初始化了一个新的StringBuilder对象。
使用JConsole检测堆内存,堆内存使用情况如下图,内存折线飙升可见Java创建了相当多的对象并占用了大量的堆内存,同时又在对这些冗余对象进行垃圾回收释放内存。
如果这里使用StringBuilder对象进行拼接:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 public class test3 { 2 public static void main(String[] args) { 3 Long dateStart=System.currentTimeMillis(); 4 StringBuilder str=new StringBuilder(); 5 for (int i=0;i<50000;i++){ 6 str.append(i); 7 } 8 Long dateEnd=System.currentTimeMillis(); 9 System.out.println("使用StringBuilder从0拼接到50000 用时: "+(dateEnd-dateStart)+"毫秒"); 10 } 11 }
执行结果: 使用StringBuilder从0拼接到50000 用时: 3毫秒
反编译代码:
Compiled from "test3.java" public class JAVASE_Fundamentals.chapter3.test3 { public JAVASE_Fundamentals.chapter3.test3(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J 3: invokestatic #3 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long; 6: astore_1 7: new #4 // class java/lang/StringBuilder 10: dup 11: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 14: astore_2 15: iconst_0 16: istore_3 17: iload_3 18: ldc #6 // int 50000 20 23: aload_2 24: iload_3 25: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 28: pop 29: iinc 3, 1 32: goto 17 35: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J 38: invokestatic #3 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long; 41: astore_3 42: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 45: new #4 // class java/lang/StringBuilder 48: dup 49: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 52: ldc #9 // String 使用StringBuilder从0拼接到50000 用时: 54: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 57: aload_3 58: invokevirtual #11 // Method java/lang/Long.longValue:()J 61: aload_1 62: invokevirtual #11 // Method java/lang/Long.longValue:()J 65: lsub 66: invokevirtual #12 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder; 69: ldc #13 // String 毫秒 71: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 74: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 77: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 80: return }
在20行到43行的循环体中,不会初始化新的StringBuilder对象
三、StringBuilder拼接字符串为何高效
StringBuilder 继承父类 AbstractStringBuilder,其成员变量有一个变量名为 value的字符数组。在AbstractStringBuilder类中定义了对value字符数组扩容等操作的方法,实际上value是一个变长的字符数组,它的实现原理与ArrayList相似。
StringBuilder也继承了父类的append方法,在拼接字符串时,实际上是把构成字符串的字符数组拷贝到value字符数组后面,这个过程不会生成新的字符串对象。StringBuilder对象使用append方法拼接的字符串的字符构成都保存在StringBuilder对象的value字符数组中。StringBuilder的toString()方法就是把这个字符数组的所有有效字符作为String类的构造器参数,得到最终的String对象
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 }
四、StringBuffer与StringBuilder的区别
StringBuffer和StringBuilder一样继承了AbstractStringBuilder,但是StringBuffer为了线程安全几乎所有方法都使用了synchronized关键字修饰。也就是说StringBuffer和StringBuilder相比,它是线程安全的,但是由于几乎每个方法在调用时都要获得对象锁并加锁最后解锁,额外的锁的操作使得StringBuffer的执行效率要低于StringBuilder。
//StringBuffer源码片段 @Override public synchronized int length() { return count; } @Override public synchronized int capacity() { return value.length; } @Override public synchronized void ensureCapacity(int minimumCapacity) { super.ensureCapacity(minimumCapacity); } /** * @since 1.5 */ @Override public synchronized void trimToSize() { super.trimToSize(); }