String StringBuffer 和 StringBuilder 的区别

目录

可变性

String 类是不可变的,其原因如下:

String 不可变原因:

  1. String 类中保存字符串的字符数组是被 final 修饰且为私有的,并且 String 类没有提供/暴露修改字符串的方法。
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
	//...
}

相关阅读:如何理解 String 类型值的不可变? - 知乎提问

StringBuilderStringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 finalprivate 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
  	//...
}

线程安全性

String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilderStringBuilderStringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacityappendinsertindexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

补充

String 类的使用

在使用 String 类型作 + 运算时,分两种情况:

  1. 两个字符串常量作为操作数:

    String str = "a" + "bc"; // str = "abc"
    System.out.println(str == "abc") // true
    

    这种情况 Java 中有常量优化机制,先在常量池中创建 "a""bc""abc",之后会由编译器优化,只在常量池中保存 "abc",再将 str 指向 "abc"。所以 str == "abc" 结果为 true

  2. 一个或两个都为 String 类对象:

    String str = new String("a") + "bc"; // str = new StringBuilder("a").append("bc").toString(); 或 new StringBuffer("a").append("bc").toString();
    // String str = new String("a") + new String("bc"); // 同理
    System.out.println(str == "abc") // false
    

    这种情况, + 运算拼接的原理是由 StringBuilder 或者 StringBuffer 类和里面的 append 方法实现拼接,然后调用 toString() 把拼接的对象转换成字符串对象,最后把得到字符串对象的地址赋值给变量。

    值得注意的是,StringBuilderStringBuffer 类的 toString() 方法会创建一个新的 String 变量,其源码:

    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }
    

    所以,第二种情况一共创建了 "a""bc" 两个常量对象及三个 String 变量,一共 5 个 String 对象,注意 new String("a") 会被 JVM 回收;而第一种情况则创建了 "a""bc""abc" 三个常量对象,同样注意在编译器优化后只存在"abc" 一个常量对象。

    相关阅读:请问下题中创建几个String对象? - Java编程宇宙的回答 - 知乎

StringBuilder 和 StringBuffer 使用

IDEA 下,某些方法中的没有对 StringBuffer 进行多线程操作时,使用了 StringBuffer 进行字符串操作,IDEA 默认会提示 'StringBuffer sb' may be declared as 'StringBuilder',而且会建议替换成 StringBuilder,因为 StringBuilder 的效率比 StringBuffer 高,IDEA 认为你没有在多线程环境下,使用 StringBuilder 可能是更好的选择。

alt

那 IDEA 的判断是否准确?基本上准确,不过限定于方法内,主要原因如下:

  • 如果 StringBuffer 是成员变量,无论如何 IDEA 都不会提示 'StringBuffer stringBuffer' may be declared as 'StringBuilder' ,因为 IDEA 无法保证 StringBuffer 不会在这个类的外部不被其他方法进行并发,即便 StringBuffer 是被 private 修饰的,没有提供对外暴露的方法,但是依然可以通过反射拿到这个 StringBuffer,进行并发操作。

  • 如果 StringBuffer 是局部变量,当在这个局部范围内(方法内)没有对 StringBuffer 的方法进行并发,并且没有将 StringBuffer 传递给别的方法,IDAE 就会提示 'StringBuffer stringBuffer' may be declared as 'StringBuilder',因为在这种情况下,外部无法拿到局部的 StringBuffer

相关阅读:idea这么牛,居然能判断当前环境线程是否安全????- CSDN

所以在方法内出现这种提示时,确实没在多线程环境下使用 StringBuffer,将来也不会修改为多线程,可以考虑直接替换为 StringBuilder,当然这仍需要结合项目的情况做选择。

参考