String,StringBuffer,StringBuilder的区别是什么?

  • 深入理解String类
  • String、StringBuffer、StringBuilder 区别
  • String、StringBuffer、StringBuilder 性能测试
  • 使用场景
  • String、StringBuffer、StringBuilder 面试题

深入理解String类

源码:\jdk1.8.0_171\src\java\lang\String.java

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;
    ......
}
  1. final修饰String类,意味着String类不能被继承。在Java早期版本中,被final修饰的方法会被转为内嵌调用以提升执行效率。而从Java SE5\6开始,就主键摒弃这种方式了。因此在现在的Java SE中,无需考虑使用final去提升调用效率。只有在确定该方法不能被覆盖时,才将方法设置为final。
  2. 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类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。”

类图

public final class String implements 
       java.io.Serializable, Comparable<String>, CharSequence {
}

public final class StringBuffer 
       extends AbstractStringBuilder 
       implements java.io.Serializable, CharSequence {
}
 
public final class StringBuilder 
       extends AbstractStringBuilder 
       implements java.io.Serializable, CharSequence {
}

执行效率:StringBuilder > StringBuffer > String

String、StringBuffer、StringBuilder 区别

异同点

  1. 都是 final 类, 都不允许被继承;
  2. String 长度是不可变的, StringBuffer、StringBuilder 长度是可变的;
  3. StringBuffer 是线程安全的, StringBuilder 不是线程安全的。
  • String 字符串常量
  • StringBuffer 字符串变量(线程安全)
  • StringBuilder 字符串变量(非线程安全)

String、StringBuffer、StringBuilder 性能测试

import org.junit.Test;

public class StringTest {
    private static int time = 50000;

    @Test
    public void testString() {
        String s = "";
        long begin = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            s += "java";
        }
        long over = System.currentTimeMillis();
        System.out.println("操作" + s.getClass().getName() + "类型使用的时间为:" + (over - begin) + "毫秒");
    }

    @Test
    public void testStringBuffer() {
        StringBuffer sb = new StringBuffer();
        long begin = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            sb.append("java");
        }
        long over = System.currentTimeMillis();
        System.out.println("操作" + sb.getClass().getName() + "类型使用的时间为:" + (over - begin) + "毫秒");
    }

    @Test
    public void testStringBuilder() {
        StringBuilder sb = new StringBuilder();
        long begin = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            sb.append("java");
        }
        long over = System.currentTimeMillis();
        System.out.println("操作" + sb.getClass().getName() + "类型使用的时间为:" + (over - begin) + "毫秒");
    }

    @Test
    public void test1String() {
        long begin = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            String s = "I" + "love" + "java";
        }
        long over = System.currentTimeMillis();
        System.out.println("字符串直接相加操作:" + (over - begin) + "毫秒");
    }

    @Test
    public void test2String() {
        String s1 = "I";
        String s2 = "love";
        String s3 = "java";
        long begin = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            String s = s1 + s2 + s3;
        }
        long over = System.currentTimeMillis();
        System.out.println("字符串间接相加操作:" + (over - begin) + "毫秒");
    }

}

执行结果(Windows 10 pro,Intellij Idea, JDK8):

操作java.lang.StringBuilder类型使用的时间为:4毫秒
操作java.lang.StringBuffer类型使用的时间为:3毫秒
字符串直接相加操作:0毫秒
操作java.lang.String类型使用的时间为:4425毫秒
字符串间接相加操作:6毫秒

上面提到string+="hello"的操作事实上会自动被JVM优化,请看下面这段代码

import org.junit.Test;

public class StringTest2 {
    private static int time = 50000;

    @Test
    public void testString() {
        String s = "";
        long begin = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            s += "java";
        }
        long over = System.currentTimeMillis();
        System.out.println("操作" + s.getClass().getName() + "类型使用的时间为:" + (over - begin) + "毫秒");
    }

    @Test
    public void testOptimalString() {
        String s = "";
        long begin = System.currentTimeMillis();
        for (int i = 0; i < time; i++) {
            StringBuilder sb = new StringBuilder(s);
            sb.append("java");
            s = sb.toString();
        }
        long over = System.currentTimeMillis();
        System.out.println("模拟JVM优化操作的时间为:" + (over - begin) + "毫秒");
    }
}

执行结果(Windows 10 pro,Intellij Idea, JDK8):

模拟JVM优化操作的时间为:2933毫秒
操作java.lang.String类型使用的时间为:3814毫秒

结论

下面对上面的执行结果进行一般性的解释:

  1. 对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如"I"+"love"+"java"; 的字符串相加,在编译期间便被优化成了"Ilovejava"。这个可以用javap -c命令反编译生成的class文件进行验证。

    对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。

  2. String、StringBuilder、StringBuffer三者的执行效率:StringBuilder > StringBuffer > String
    当然这个是相对的,不一定在所有情况下都是这样。比如String str = "hello"+ "world"的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高。
    因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:

  • 当字符串相加操作或者改动较少的情况下,建议使用 String str="hello"这种形式;
  • 当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。

使用场景

  • 使用String类的场景:在字符串不经常变化的场景中可以使用String类,例如常量的声明、少量的变量运算。
  • 使用StringBuffer类的场景:在频繁进行字符串运算(如拼接、替换、删除等),并且运行在多线程环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装。
  • 使用StringBuilder类的场景:在频繁进行字符串运算(如拼接、替换、和删除等),并且运行在单线程的环境中,则可以考虑使用StringBuilder,如SQL语句的拼装、JSON封装等。

解析

String S1 = "This is only a" + " simple" + " test";
StringBuffer Sb = new StringBuilder("This is only a").append(" simple").append(" test");

生成对象的速度:S1 > Sb, 上面的示例中StringBuffer速度上并不占优势。

String S1 = "This is only a" + " simple" + " test";
# 其实就是:
String S1 = "This is only a simple test";

需要注意的是:如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如:

String S2 = "This is only a";
String S3 = "simple";
String S4 = "test";
String S1 = S2 +S3 + S4;

这时候 JVM 会规规矩矩的按照原来的方式去做。

String、StringBuffer、StringBuilder 面试题

  1. 下面这段代码的输出结果是什么?
   String a = "hello2";
   String b = "hello" + 2;
   System.out.println((a == b));//输出结果为:true

输出结果为:true。原因很简单,"hello"+2在编译期间就已经被优化成"hello2",因此在运行期间,变量a和变量b指向的是同一个对象。

  1. 下面这段代码的输出结果是什么?
    String a = "hello2";
    String b = "hello";
    String c = b + 2;
    System.out.println((a == c));//输出结果为:false

输出结果为:false。由于有符号引用的存在,所以 String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,因此这种方式生成的对象事实上是保存在堆上的。因此a和c指向的并不是同一个对象。javap -c得到的内容:

PS D:\opt\workspace\g\framework.xyzla\framework.xyzla\blog\blog.h5\target\test-classes> javap -c StringT
Compiled from "StringT.java"
public class StringT {
  public StringT();
    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 hello2
       2: astore_1
       3: ldc           #3                  // String hello
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_2
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: iconst_2
      18: invokevirtual #7                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      21: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      24: astore_3
      25: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      28: aload_1
      29: aload_3
      30: if_acmpne     37
      33: iconst_1
      34: goto          38
      37: iconst_0
      38: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
      41: return
}
  1. 下面这段代码的输出结果是什么?
    String a = "hello2";
    final String b = "hello";
    String c = b + 2;
    System.out.println((a == c));//输出结果为:true

输出结果为:true。对于被final修饰的变量,会在class文件常量池中保存一个副本,也就是说不会通过连接而进行访问,对final变量的访问在编译期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = "hello" + 2; 下图是javap -c的内容:

PS D:\opt\workspace\g\framework.xyzla\framework.xyzla\blog\blog.h5\target\test-classes> javap -c StringT3
Compiled from "StringT3.java"
public class StringT3 {
  public StringT3();
    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 hello2
       2: astore_1
       3: ldc           #3                  // String hello
       5: astore_2
       6: ldc           #2                  // String hello2
       8: astore_3
       9: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      12: aload_1
      13: aload_3
      14: if_acmpne     21
      17: iconst_1
      18: goto          22
      21: iconst_0
      22: invokevirtual #5                  // Method java/io/PrintStream.println:(Z)V
      25: return
}
  1. 下面这段代码输出结果为:
public class StringT {
    public static void main(String[] args) {
        String a = "hello2";
        final String b = getHello();
        String c = b + 2;
        System.out.println((a == c));//输出结果为:false
    }

    public static String getHello() {
        return "hello";
    }
}

输出结果为false。这里面虽然将b用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此a和c指向的不是同一个对象。

  1. 下面这段代码的输出结果是什么?
public class StringT {
    public static void main(String[] args) {
        String a = "hello";
        String b = new String("hello");
        String c = new String("hello");
        String d = b.intern();

        System.out.println(a == b);//输出结果为:false
        System.out.println(b == c);//输出结果为:false
        System.out.println(b == d);//输出结果为:false
        System.out.println(a == d);//输出结果为:true
    }
}

这里面涉及到的是String.intern方法的使用。在String类中,intern方法是一个本地方法,在JAVA SE6之前,intern方法会在运行时常量池中查找是否存在内容相同的字符串,如果存在则返回指向该字符串的引用,如果不存在,则会将该字符串入池,并返回一个指向该字符串的引用。因此,a和d指向的是同一个对象。

  1. String str = new String("abc");创建了多少个对象?

这个问题在很多书籍上都有说到比如《Java程序员面试宝典》,包括很多国内大公司笔试面试题都会遇到,大部分网上流传的以及一些面试书籍上都说是2个对象,这种说法是片面的。
如果有不懂得地方可以参考这篇帖子:http://rednaxelafx.iteye.com/blog/774673/
首先必须弄清楚创建对象的含义,创建是什么时候创建的?这段代码在运行期间会创建2个对象么?毫无疑问不可能,用javap -c反编译即可得到JVM执行的字节码内容:

PS D:\opt\workspace\g\framework.xyzla\framework.xyzla\blog\blog.h5\target\test-classes> javap -c StringT
Compiled from "StringT.java"
public class StringT {
  public StringT();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/String
       3: dup
       4: ldc           #3                  // String abc
       6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: return
}

很显然,new只调用了一次,也就是说只创建了一个对象。

而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。而为什么大家都在说是2个对象呢,这里面要澄清一个概念 该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。
因此,这个问题如果换成String str = new String("abc");涉及到几个String对象?合理的解释是2个。
个人觉得在面试的时候如果遇到这个问题,可以向面试官询问清楚“是这段代码执行过程中创建了多少个对象还是涉及到多少个对象”再根据具体的来进行回答。

  1. 下面这段代码1)和2)的区别是什么? 本题错误,两者效率一样,编译后用jd-gui 查看反编译代码结果一致。 jdk1.6.0_45\jdk1.7.0_80\jdk1.8.0_181
public class StringT {
    public static void main(String[] args) {
        String str1 = "I";
        str1 += "love" + "java";  //1

        String str2 = "I";
        str2 = str2 + "love" + "java"; //2
    }
}

1)的效率比2)的效率要高,1)中的"love"+"java"在编译期间会被优化成"lovejava",而2)中的不会被优化。下面是两种方式的字节码: 的字节码:

PS D:\opt\workspace\g\framework.xyzla\framework.xyzla\blog\blog.h5\target\test-classes> javap -c StringT
Compiled from "StringT.java"
public class StringT extends java.lang.Object{
public StringT();
  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 I
   2:   astore_1
   3:   new     #3; //class java/lang/StringBuilder
   6:   dup
   7:   invokespecial   #4; //Method java/lang/StringBuilder."<init>":()V
   10:  aload_1
   11:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   14:  ldc     #6; //String lovejava
   16:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   19:  invokevirtual   #7; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   22:  astore_1
   23:  ldc     #2; //String I
   25:  astore_2
   26:  new     #3; //class java/lang/StringBuilder
   29:  dup
   30:  invokespecial   #4; //Method java/lang/StringBuilder."<init>":()V
   33:  aload_2
   34:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   37:  ldc     #6; //String lovejava
   39:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   42:  invokevirtual   #7; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   45:  astore_2
   46:  return

}

可以看出,在1)中只进行了一次append操作,而在2)中进行了两次append操作。

探秘Java中的String、StringBuilder以及StringBuffer

  • qq_43638135
    妲己再美究为妃: 博主没有想过自己接一些私活干吗?我现在还没毕业,但是我也确实听说外挂市场自动化游戏脚本市场挺火热的,并且报酬也很丰厚,但是具体的我也不是很清楚,求解答。 (1个月前 #47楼) 查看回复(2) 举报 回复
    22