(三)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文件其默认编码方式。

代码示例:

 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     }
View Code

运行结果:

 注:UTF-16编码下前缀的FEFF是用来标记字节顺序的。比如说对于UTF-16,如果前缀是FEFF,表明这个字节流是Big-Endian的;如果前缀是FFFE,就表明这个字节流是Little-Endian的。所以字节数比实际还要增加两个字节。

 

 

二、String对象

 

1、String对象是不可变的,用String类中的方法在对String进行修改时,基本都是创建了一个新的String对象,而最初的String对象并未改变。以下来测试一下数个字符串用  +  号拼接。

代码示例(实验环境:JDK8):

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 }
View Code

用   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,使用 + 来拼接

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     }
View Code

执行结果:   使用 +拼接字符串 从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对象进行拼接:

 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 }
View Code

执行结果: 使用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();
    }

 

posted @ 2021-03-03 09:19  初袋霸王龙  阅读(173)  评论(0编辑  收藏  举报