zoukankan      html  css  js  c++  java
  • 5.浅谈String、StringBuilder和StringBuffer

     5.1

      5.1.1 String 

        有如下代码 请问生成了几个对象呢?

    String str = "hello";
    str = "hello" + "world";

         没错答案是三个,分别是“hello”、“world” 和 “helloworld” 

        那么这串代码创建了几个对象了

    String str = "aaa";
    String name = "aaa";
    String address = "bbb";
    String str = str + name + address + "ccc";

        总共是四个,分别是 “aaa” 、“bbb” 、"ccc" 和 “aaaaaabbbccc” 。

        这是什么情况?

        因为首先 String 声明出来的值是不能改变的,是常量。所以说在创建 str 引用的时候声明了第一个 “aaa” 对象,而在声明 name 的时候,会先去常量池内找有没有值是 “aaa” 的,找到了就不用了建立新的对象了,而在创建 address 的时候因为常量池内没有 “bbb” 所以又创建了一个新的对象,再为 str 重新赋值的时候,又因为找不到 “ccc” 和 “aaaaaabbbccc” ,所以为这两个值创建了对象,所以总共创建了四个对象。关于对比 String = String 的问题请向下看序言。

      5.1.2 StringBuilder 和 StringBuffer

        为什么要把这两个放在一起,第一呢 这两个都继承于 AbstractStringBuilder 类。这两个除了有线程安全与否的大不同外,基本特性是一样的。

        一般来说,我们在对字符串进行修改的时候,会使用 StringBuffer 和 StringBuilder,和String最大一点的不同是:StringBuffer 和 StringBuilder 的对象可以被多次修改,而且不会产生新的对象。 所以一般牵涉到拼接字符串的工作都会使用这两个而不是 String 这样会大大减少内存的浪费。

        这两个对比起来呢,一般情况下我们又使用 StringBuilder 更多一些,因为 StringBuilder 的速度会更快一些。因为 StringBuilder 是线程不安全的(可以同步访问),每次访问不需要判断锁 synchronized 的存在,而 StringBuffer 是线程安全的,是需要判断的,所以大多数情况下,我们都会使用 StringBuilder ,只有在应用程序要求线程安全的情况下,才必须使用 StringBuffer。

       

    有这么一段代码:

    复制代码
    1 public class TestMain
    2 {
    3     public static void main(String[] args)
    4     {
    5         String str0 = "123";
    6         String str1 = "123";
    7         System.out.println(str0 == str1);
    8     }
    9 }
    复制代码

    运行结果是什么?答案当然是true。对,答案的确是true,但是这是为什么呢?很多人第一反应肯定是两个"123"的String当然相等啊,这还要想。但是"=="在Java比较的不是两个对象的值,而是比较两个对象的引用是否相等,和两个String都是"123"又有什么关系呢?或者我们把程序修改一下

    复制代码
    1 public class TestMain
    2 {
    3     public static void main(String[] args)
    4     {
    5         String str2 = new String("234");
    6         String str3 = new String("234");
    7         System.out.println(str2 == str3);
    8     }
    9 }
    复制代码

    这时候运行结果就是false了,因为尽管两个String对象都是"234",但是str2和str3是两个不同的引用,所以返回的false。OK,围绕第一段代码返回true,第二段代码返回false,开始文章的内容。

    为什么String=String?

    在JVM中有一块区域叫做常量池,关于常量池,我在写虚拟机的时候有专门提到http://www.cnblogs.com/xrq730/p/4827590.html。常量池中的数据是那些在编译期间被确定,并被保存在已编译的.class文件中的一些数据。除了包含所有的8种基本数据类型(char、byte、short、int、long、float、double、boolean)外,还有String及其数组的常量值,另外还有一些以文本形式出现的符号引用。

    Java栈的特点是存取速度快(比堆块),但是空间小,数据生命周期固定,只能生存到方法结束。我们定义的boolean b = true、char c = 'c'、String str = “123”,这些语句,我们拆分为几部分来看:

    1、true、c、123,这些等号右边的指的是编译期间可以被确定的内容,都被维护在常量池中

    2、b、c、str这些等号左边第一个出现的指的是一个引用,引用的内容是等号右边数据在常量池中的地址

    3、boolean、char、String这些是引用的类型

    栈有一个特点,就是数据共享。回到我们第一个例子,第五行String str0 = "123",编译的时候,在常量池中创建了一个常量"123",然后走第六行String str1 = "123",先去常量池中找有没有这个"123",发现有,str1也指向常量池中的"123",所以第七行的str0 == str1返回的是true,因为str0和str1指向的都是常量池中的"123"这个字符串的地址。当然如果String str1 = "234",就又不一样了,因为常量池中没有"234",所以会在常量池中创建一个"234",然后str1代表的是这个"234"的地址。分析了String,其实其他基本数据类型也都是一样的:先看常量池中有没有要创建的数据,有就返回数据的地址,没有就创建一个

    第二个例子呢?Java虚拟机的解释器每遇到一个new关键字,都会在堆内存中开辟一块内存来存放一个String对象,所以str2、str3指向的堆内存中虽然存储的是相等的"234",但是由于是两块不同的堆内存,因此str2 == str3返回的仍然是false,网上找到一张图表示一下这个概念:

    为什么要使用StringBuilder和StringBuffer拼接字符串?

    大家在开发中一定有一个原则是"利用StringBuilder和StringBuffer拼接字符串",但是为什么呢?用一段代码来分析一下:

    复制代码
     1 public class StringTest {
     2 
     3     @Test
     4     public void testStringPlus() {
     5         String str = "111";
     6         str += "222";
     7         str += "333";
     8         System.out.println(str);
     9     }
    10     
    11 }
    复制代码

    这段代码,我们找到编译后的StringTest.class文件,使用"javap -verbose StringTest"或者"javap -c StringTest"都可以,反编译一下class获取到对应的字节码:

    复制代码
      public void testStringPlus();
        Code:
           0: ldc           #17                 // String 111
           2: astore_1
           3: new           #19                 // class java/lang/StringBuilder
           6: dup
           7: aload_1
           8: invokestatic  #21                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)L
    java/lang/String;
          11: invokespecial #27                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/S
    tring;)V
          14: ldc           #30                 // String 222
          16: invokevirtual #32                 // Method java/lang/StringBuilder.append:(Ljava/lang/Str
    ing;)Ljava/lang/StringBuilder;
          19: invokevirtual #36                 // Method java/lang/StringBuilder.toString:()Ljava/lang/
    String;
          22: astore_1
          23: new           #19                 // class java/lang/StringBuilder
          26: dup
          27: aload_1
          28: invokestatic  #21                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)L
    java/lang/String;
          31: invokespecial #27                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/S
    tring;)V
          34: ldc           #40                 // String 333
          36: invokevirtual #32                 // Method java/lang/StringBuilder.append:(Ljava/lang/Str
    ing;)Ljava/lang/StringBuilder;
          39: invokevirtual #36                 // Method java/lang/StringBuilder.toString:()Ljava/lang/
    String;
          42: astore_1
          43: getstatic     #42                 // Field java/lang/System.out:Ljava/io/PrintStream;
          46: aload_1
          47: invokevirtual #48                 // Method java/io/PrintStream.println:(Ljava/lang/String
    ;)V
          50: return
    }
    复制代码

    这段字节码不用看得很懂,大致上能明白就好,意思很明显:编译器每次碰到"+"的时候,会new一个StringBuilder出来,接着调用append方法,在调用toString方法,生成新字符串

    那么,这意味着,如果代码中有很多的"+",就会每个"+"生成一次StringBuilder,这种方式对内存是一种浪费,效率很不好。

    在Java中还有一种拼接字符串的方式,就是String的concat方法,其实这种方式拼接字符串也不是很好,具体原因看一下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);
    }
    复制代码

    意思就是通过两次字符串的拷贝,产生一个新的字符数组buf[],再根据字符数组buf[],new一个新的String对象出来,这意味着concat方法调用N次,将发生N*2次数组拷贝以及new出N个String对象,无论对于时间还是空间都是一种浪费。

    根据上面的解读,由于"+"拼接字符串与String的concat方法拼接字符串的低效,我们才需要使用StringBuilder和StringBuffer来拼接字符串。以StringBuilder为例:

    复制代码
     1 public class TestMain
     2 {
     3     public static void main(String[] args)
     4     {
     5         StringBuilder sb = new StringBuilder("111");
     6         sb.append("222");
     7         sb.append("111");
     8         sb.append("111");
     9         sb.append("444");
    10         System.out.println(sb.toString());
    11     }
    12 }
    复制代码

    StringBuffer和StringBuilder原理一样,无非是在底层维护了一个char数组,每次append的时候就往char数组里面放字符而已,在最终sb.toString()的时候,用一个new String()方法把char数组里面的内容都转成String,这样,整个过程中只产生了一个StringBuilder对象与一个String对象,非常节省空间。StringBuilder唯一的性能损耗点在于char数组不够的时候需要进行扩容,扩容需要进行数组拷贝,一定程度上降低了效率

    StringBuffer和StringBuilder用法一模一样,唯一的区别只是StringBuffer是线程安全的,它对所有方法都做了同步,StringBuilder是线程非安全的,所以在不涉及线程安全的场景,比如方法内部,尽量使用StringBuilder,避免同步带来的消耗。

    另外,StringBuffer和StringBuilder还有一个优化点,上面说了,扩容的时候有性能上的损耗,那么如果可以估计到要拼接的字符串的长度的话,尽量利用构造函数指定他们的长度。

    真的不能用"+"拼接字符串?

    虽然说不要用"+"拼接字符串,因为会产生大量的无用StringBuilder对象,但也不是不可以,比如可以使用以下的方式:

    复制代码
    1 public class TestMain
    2 {
    3     public static void main(String[] args)
    4     {
    5         String str = "111" + "222" + "333" + "444";
    6         System.out.println(str);
    7     }
    8 }
    复制代码

    就这种连续+的情况,实际上编译的时候JVM会只产生一个StringBuilder并连续append等号后面的字符串。

    不过上面的例子要注意一点,因为"111"、"222"、"333"、"444"都是编译期间即可得知的常量,因为第5行的代码JVM在编译的时候并不会生成一个StringBuilder而是直接生成字符串"111222333444"

    但是这么写得很少,主要原因有两点:

    1、例子比较简单,但实际上大量的“+”会导致代码的可读性非常差

    2、待拼接的内容可能从各种地方获取,比如调用接口、从.properties文件中、从.xml文件中,这样的场景下尽管用多个“+”的方式也不是不可以,但会让代码维护性不太好

    ======================================================================

    本文部分内容转载 五月的仓颉 :从为什么String = String 谈到 StringBuilder和StringBuffer https://www.cnblogs.com/xrq730/p/4841518.html

    ======================================================================

  • 相关阅读:
    网络爬虫技术总结
    MongoDB(7):集群部署实践,包含复制集,分片
    MongoDB(6):简单配置,环境变量,关闭服务
    Mongod(5):启动命令mongod参数说明
    MongoDB(4):多种方式关闭服务命令
    Linux下的Source命令及脚本的执行方式解析
    linux第一天
    echarts 各种细节问题
    MongoDB(3):小的细节问题
    获取用户Ip地址通用方法常见安全隐患(HTTP_X_FORWARDED_FOR)
  • 原文地址:https://www.cnblogs.com/mokpiet/p/13656390.html
Copyright © 2011-2022 走看看