zoukankan      html  css  js  c++  java
  • Java 中的String、StringBuilder与StringBuffer的区别联系(转载)

      

     1 String 基础

      想要了解一个类,最好的办法就是看这个类的源代码,String类源代码如下:

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
    
        /** Cache the hash code for the string */
        private int hash; // Default to 0
    
        /** use serialVersionUID from JDK 1.0.2 for interoperability */
        private static final long serialVersionUID = -6849794470754667710L;
    
        /**
         * Class String is special cased within the Serialization Stream Protocol.
         *
         * A String instance is written into an ObjectOutputStream according to
         * <a href="{@docRoot}/../platform/serialization/spec/output.html">
         * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
         */
        private static final ObjectStreamField[] serialPersistentFields =
            new ObjectStreamField[0];

      从上面代码可以看出:

      ① String类是final类,即意味着String类不能被继承,并且它的成员方法都默认为final方法。

      ② 上面列出了String类的成员属性,String类其实是通过char数组来保存字符串的。

      再来看String类的一些方法:

     public String substring(int beginIndex, int endIndex) {
            if (beginIndex < 0) {
                throw new StringIndexOutOfBoundsException(beginIndex);
            }
            if (endIndex > value.length) {
                throw new StringIndexOutOfBoundsException(endIndex);
            }
            int subLen = endIndex - beginIndex;
            if (subLen < 0) {
                throw new StringIndexOutOfBoundsException(subLen);
            }
            return ((beginIndex == 0) && (endIndex == value.length)) ? this
                    : new String(value, beginIndex, subLen);
     }
    
      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);
     }
     public String replace(char oldChar, char newChar) {
            if (oldChar != newChar) {
                int len = value.length;
                int i = -1;
                char[] val = value; /* avoid getfield opcode */
    
                while (++i < len) {
                    if (val[i] == oldChar) {
                        break;
                    }
                }
                if (i < len) {
                    char buf[] = new char[len];
                    for (int j = 0; j < i; j++) {
                        buf[j] = val[j];
                    }
                    while (i < len) {
                        char c = val[i];
                        buf[i] = (c == oldChar) ? newChar : c;
                        i++;
                    }
                    return new String(buf, true);
                }
            }
            return this;
     }

       上面三个方法,无论是substring、concat、replace操作都不是在原来的基础上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串没有改变。

      牢记: 对String对象的任何改变都不会影响到原对象,相关的任何change操作都会产生新的对象

      2 深入理解String、StringBuffer、StringBuilder

      (1) String str = "Hello" 与 String str = new String("Hello") 的区别

    1 String str1="Hello latiny";
    2 String str2="Hello latiny";
    3 String str3=new String("Hello latiny");
    4 String str4=new String("Hello latiny");
    5 
    6 System.out.println(str1==str2);
    7 System.out.println(str1==str3);
    8 System.out.println(str3==str4);

      结果为:

    true
    false
    false

      

      为什么会出现这样的结果?这里我们作一个详细解释

      ① 首先得引入常量池的概念,常量池指的是在编译期就被确定,并保存在已编译的.class文件中的一些数据。它包含了类、方法、接口等的常量,也包含了字符串常量。

    String str1="Hello latiny"

    String str2="Hello latiny";

    其中 Hello latiny 都是字符串常量。它们在编译时就被确定了,所以str1 == str2为true

    ② 用new String()创建的字符串不是常量,不能在编译时确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。更进一步解释一下,通过new 关键字创建的对象是在堆区进行的,而在堆区进行对象的生成过程是不会去检测该对象是否已经存在。因此通过new创建的对象,一定是不同的对象,即使字符串内容相同。

     

    (2) 既然在Java中已经存在了String类,为什么还需要StringBuffer与StringBuilder类呢?

    看下面这段代码:

     1 public class StringTest {
     2 
     3     public static void main(String[] args) {
     4     
     5         String str = "";
     6         for(int i=0; i<10000; i++)
     7         {
     8             str+="Hello";
     9         }
    10         
    11     }
    12 
    13 }

      

     str+="Hello"; 这句代码的过程相当于将原有的 str 变量指向的对象内容取出与 Hello 作字符串相加操作,再存进另一个新的String 对象中,再让str变量的指向新生成的对象。反编译其字节码文件就知道了:
    C:WorkProjectJavaEclipseJustTestincomlatinystring>javap -c StringTest
    警告: 二进制文件StringTest包含com.latiny.string.StringTest
    Compiled from "StringTest.java"
    public class com.latiny.string.StringTest {
      public com.latiny.string.StringTest();
        Code:
           0: aload_0
           1: invokespecial #8                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: ldc           #16                 // String
           2: astore_1
           3: iconst_0
           4: istore_2
           5: goto          31
           8: new           #18                 // class java/lang/StringBuilder
          11: dup
          12: aload_1
          13: invokestatic  #20                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
          16: invokespecial #26                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
          19: ldc           #29                 // String Hello
          21: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          24: invokevirtual #35                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
          27: astore_1
          28: iinc          2, 1
          31: iload_2
          32: sipush        10000
          35: if_icmplt     8
          38: return
    }

    从这段反编译的字节码文件可以看出:从第8行到35行是整个循环执行过程, 并且每次循环都会new 出一个StringBuilder对象,然后进行append操作,最后通过toString 方法返回String对象。也就是说这个循环执行完毕new出了10000个对象, 这是非常大的资源浪费。从上面还可以看出:str+="Hello";  自动会被JVM优化成:

    StringBuilder str1 = new StringBuilder(str);

    str1.append("Hello");

    str = str1.toString();

     

    再来看下面这段代码:

    public class StringTest {
    
    	public static void main(String[] args) {
    	
    		StringBuilder str = new StringBuilder();
    		for(int i=0; i<10000; i++)
    		{
    			str.append("Hello");
    		}
    		
    	}
    
    }
    

      

    反编译字节码文件得到如下代码:

    C:WorkProjectJavaEclipseJustTestincomlatinystring>javap -c StringTest
    警告: 二进制文件StringTest包含com.latiny.string.StringTest
    Compiled from "StringTest.java"
    public class com.latiny.string.StringTest {
      public com.latiny.string.StringTest();
        Code:
           0: aload_0
           1: invokespecial #8                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: new           #16                 // class java/lang/StringBuilder
           3: dup
           4: invokespecial #18                 // Method java/lang/StringBuilder."<init>":()V
           7: astore_1
           8: iconst_0
           9: istore_2
          10: goto          23
          13: aload_1
          14: ldc           #19                 // String Hello
          16: invokevirtual #21                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          19: pop
          20: iinc          2, 1
          23: iload_2
          24: sipush        10000
          27: if_icmplt     13
          30: return
    }

      

    上面代码可以看出,循环从13行到27行结束,并且new 操作只进行了一次,也就是说只产生了一个对象, append操作是在原有对象的基础上进行的。因此循环10000次之后,这段代码所占的资源比直接使用String定义的变量要小得多

     

    那有人又要问了既然有了StringBuilder类,为什么还需要StringBuffer类,查看源代码就知道了,StringBuilder与StringBuffer类拥有的成员属性及方法基本相同,区别是StringBuffer类的成员方法多了一个关键字: synchronized,很明显这个关键字是在多线程访问时起到安全保护作用的,即StringBuffer是线程安全的。

    StringBuilder的insert方法:

    1 @Override
    2     public StringBuilder insert(int offset, Object obj) {
    3             super.insert(offset, obj);
    4             return this;
    5     }

     

    StringBuffer的insert方法:

    1     @Override
    2     public synchronized StringBuffer insert(int index, char[] str, int offset,
    3                                             int len)
    4     {
    5         toStringCache = null;
    6         super.insert(index, str, offset, len);
    7         return this;
    8     }

      3 三个类不同场景的性能测试

    public class StringTest {
    
        private static final int TIMES = 50000;
        
        public static void main(String[] args) {
            TestString1();
            TestBuilder();
            TestBuffer();
            
            // 直接字符相加与间接字符相加对比
            TestString2();
            TestString3();
        }
        
        public static void  TestString1()
        {
            String str = "";
            long begin = System.currentTimeMillis();
            for(int i=0; i<TIMES; i++)
            {
                str += "Java";
            }
            long end = System.currentTimeMillis();
            
            System.out.println("操作"+str.getClass().getName()+"类型需要的时间:"+(end-begin)+"毫秒");
        }
        
        public static void  TestBuilder()
        {
            StringBuilder str = new StringBuilder();
            long begin = System.currentTimeMillis();
            for(int i=0; i<TIMES; i++)
            {
                str.append("Java");
            }
            long end = System.currentTimeMillis();
            
            System.out.println("操作"+str.getClass().getName()+"类型需要的时间:"+(end-begin)+"毫秒");
        }
        
        public static void  TestBuffer()
        {
            StringBuffer str = new StringBuffer();
            long begin = System.currentTimeMillis();
            for(int i=0; i<TIMES; i++)
            {
                str.append("Java");
            }
            long end = System.currentTimeMillis();
            
            System.out.println("操作"+str.getClass().getName()+"类型需要的时间:"+(end-begin)+"毫秒");
        }
    
        public static void  TestString2()
        {
            String str = "";
            long begin = System.currentTimeMillis();
            for(int i=0; i<TIMES; i++)
            {
                str="I"+"Love"+"Java";
            }
            long end = System.currentTimeMillis();
            
            System.out.println("字符串直接相加操作需要的时间:"+(end-begin)+"毫秒");
        }
        
        public static void  TestString3()
        {
            String str1 = "I";
            String str2 = "Love";
            String str3 = "Java";
            String str = "";
            long begin = System.currentTimeMillis();
            for(int i=0; i<TIMES; i++)
            {
                str = str1+str2+str3;
            }
            long end = System.currentTimeMillis();
            
            System.out.println("字符串间接相加操作需要的时间:"+(end-begin)+"毫秒");
        }
        
    }

    测试环境:win7 + Eclipse + JDK1.8

      结果为:

    操作java.lang.String类型需要的时间:5395毫秒
    操作java.lang.StringBuilder类型需要的时间:1毫秒
    操作java.lang.StringBuffer类型需要的时间:2毫秒


    字符串直接相加操作需要的时间:1毫秒
    字符串间接相加操作需要的时间:4毫秒
    请按任意键继续. . .

      上面提到JVM会自动优化 str+="Hello",看如下代码:

    public static void  TestString1()
        {
            String str = "";
            long begin = System.currentTimeMillis();
            for(int i=0; i<time; i++)
            {
                str+="Java";
            }
            long end = System.currentTimeMillis();
            
            System.out.println("操作"+str.getClass().getName()+"类型需要的时间:"+(end-begin)+"毫秒");
        }
        
        public static void  TestString1Optimal()
        {
            String str = "";
            long begin = System.currentTimeMillis();
            for(int i=0; i<time; i++)
            {
                StringBuilder str1 = new StringBuilder(str);
    
                str1.append("Java");
    
                str = str1.toString();
            }
            long end = System.currentTimeMillis();
            
            System.out.println("模拟JVM优化操作需要的时间:"+(end-begin)+"毫秒");
        }

      执行结果:

    操作java.lang.String类型需要的时间:11692毫秒
    模拟JVM优化操作需要的时间:9958毫秒

      得到验证。

      对执行结果进行一般的解释:

    ① 对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如 "I"+"like"+"Java" 的字符串相加,在编译时被优化成 "Ilikejava"。对于间接相加(即包含字符串引用),刑如:str=str1+str2+str3 ,效率比直接相加低,因为在编译时编译器不会对引用变量进行优化。

    ② 三者的效率:

    StringBuilder > StringBuffer > String

    但是这只是相对的,如String str = "Hello" + "Latiny"; 比 StringBuilder str = new StringBuilder().append("Hello").append("Latiny"); 要高。

    这三个类各有利弊,根据不同的情况选择使用:

    当字符串相加操作或者改动较少时,使用String类;

    当字符串相加操作较多时,使用StringBuilder吗,如果采用多线程,需要考虑线程安全则使用StringBuffer;

     

    4 常见的String、 StringBuilder、StringBuffer面试题

    (1) String a = "Hello2"; final String b = "Hello"; String c = b+"2"; System.out.println(a==c); 输出结果为true

    (2) 下面代码输出结果为false:

     1     public static void main(String[] args) {
     2         // TODO Auto-generated method stub
     3         //TestString1();
     4         //TestString1Optimal();
     5         String a = "Hello2"; 
     6         final String b = getHello(); 
     7         String c = b+"2"; System.out.println(a==c);
     8     }
     9     
    10     public static String getHello()
    11     {
    12         return "Hello";
    13     }

      (3) 下面代码输出为true:

    1 public static void main(String[] args) {
    2         // TODO Auto-generated method stub
    3         //TestString1();
    4         //TestString1Optimal();
    5         String a = "Hello2"; 
    6         String b = a.intern(); 
    7         System.out.println(a==b);
    8     }

    (4) 代码 I 与 II 的区别

    1     String str = "I"; 
    2     str += "like" + "Java";  //I
    3     //str = str+"like" + "Java"; //II

      ① I的效率比II 高,I的 "like" + "Java" 在编译时会被优化为 likejava 而II 的不会

      I 的反编译字节码

    public class com.latiny.string.StringTest1 {
    public com.latiny.string.StringTest1();
    Code:
    0: aload_0
    1: invokespecial #8 // Method java/lang/Object."<init>":()V
    4: return
    
    public static void main(java.lang.String[]);
    Code:
    0: ldc #16 // String I
    2: astore_1
    3: new #18 // class java/lang/StringBuilder
    6: dup
    7: aload_1
    8: invokestatic #20 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
    11: invokespecial #26 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
    14: ldc #29 // String likeJava
    16: invokevirtual #31 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
    19: invokevirtual #35 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
    22: astore_1
    23: return
    }

      II 的反编译字节码

      

    public class com.latiny.string.StringTest1 {
      public com.latiny.string.StringTest1();
        Code:
           0: aload_0
           1: invokespecial #8                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: ldc           #16                 // String I
           2: astore_1
           3: new           #18                 // class java/lang/StringBuilder
           6: dup
           7: aload_1
           8: invokestatic  #20                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
          11: invokespecial #26                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
          14: ldc           #29                 // String like
          16: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          19: ldc           #35                 // String Java
          21: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          24: invokevirtual #37                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
          27: astore_1
          28: return
    }

      可以看出 I 中只进行了一次append操作,II 的进行了两次。

     

    转自:http://www.cnblogs.com/dolphin0520/p/3778589.html

     

     

     

     

     

  • 相关阅读:
    第一本书 第七章(课后题)
    java基础小测试
    随笔1
    随笔
    日记 晴 2017.7.30
    自我介绍
    与或非逻辑运算符 与或非位运算符
    日记1 天气阴 阵雨
    归并排序的两个版本实现代码
    Winedt打开tex文件报错error reading的解决方案
  • 原文地址:https://www.cnblogs.com/Latiny/p/8289523.html
Copyright © 2011-2022 走看看