String、StringBuffer和StringBuilder的区别

224

请告诉我一个实时情景来比较 StringStringBufferStringBuilder

12个回答

397

可变性的区别:

String不可变的,如果你试图改变它们的值,会创建另一个对象,而StringBufferStringBuilder可变的,因此它们可以改变它们的值。

线程安全的区别:

StringBufferStringBuilder的区别在于StringBuffer是线程安全的。因此,当应用程序只需要在单个线程中运行时,最好使用StringBuilderStringBuilderStringBuffer更高效。

应用场景:

  • 如果您的字符串不需要更改,请使用String类,因为String对象是不可变的。
  • 如果您的字符串可能更改(例如:在构建字符串时涉及大量逻辑和操作),并且仅从单个线程访问,则使用StringBuilder就足够了。
  • 如果您的字符串可能更改,并且将从多个线程访问,请使用StringBuffer,因为StringBuffer是同步的,可以提供线程安全性。

18
在逻辑运算中使用String类型会非常缓慢,因为JVM会在字节码中将String转换为StringBuffer。从String到StringBuffer再返回String的转换过程会浪费很多额外开销,因此不建议使用String类型。 - Pieter van Niekerk
2
所以在“字符串”中,当我们改变值时,另一个对象被创建。旧对象引用是否被置空,以便可以由“GC”进行垃圾回收,或者它甚至被垃圾回收了吗? - Harsh Vardhan
@PietervanNiekerk 逻辑操作是什么意思? - emlai
逻辑操作指的是基本的字符串操作。现在我想问一个问题,正如@Peter所说,我们是否应该在所有情况下都使用StringBuffer而不是String,还是有一些特定的情况? - JavaDragon
@bakkal 我可以在 StringBuilder 中使用 String 的所有方法吗? - roottraveller

48
  • 当需要使用不可变的结构时,请使用String类型;从一个String中获取新字符序列可能会带来无法接受的性能损失,无论是在CPU时间还是内存方面(获取子字符串是CPU效率高的方法,因为数据不会被复制,但这意味着可能仍然分配了大量的数据)。
  • 当需要创建可变的字符序列,通常是将多个字符序列连接在一起时,请使用StringBuilder类型。
  • 当多个线程正在读取/修改字符串缓冲区时,需对基础字符串进行同步更改时,请使用StringBuffer类型,使用情况与StringBuilder相同。

参见示例此处


2
简洁明了,但它缺少使用StringBuilder/Buffer的根本原因,那就是减少或消除常规字符串连接行为的重新分配和数组复制。 - user177800
1
“当你处理不可变字符串时,使用String”这句话并没有意义。String的实例是不可变的,所以也许应该这样写:“当由于不可变性而导致内存使用量不重要时,请使用String”。接受的答案已经很好地涵盖了这一点。 - fooMonster

29

基础知识:

String 是一个不可变的类,它不能被更改。 StringBuilder 是一个可变的类,可以添加、替换或删除字符,并最终转换为 StringStringBufferStringBuilder 的原始同步版本。

在只有单个线程访问对象的所有情况下,应优先选择 StringBuilder

详细信息:

还要注意,StringBuilder/Buffer 并不是魔法,它们只是使用数组作为支持对象,每当数组满时就必须重新分配。一定要最初创建足够大的 StringBuilder/Buffer 对象,以避免每次调用 .append() 时都需要不断地重新调整大小。

重新调整大小可能会变得非常恶化。它基本上将支持数组的大小每次扩展时增加到当前大小的两倍。当 StringBuilder/Buffer 类开始变得庞大时,这可能会导致大量未使用的 RAM 被分配。

在Java中,String x = "A" + "B"; 在幕后使用了一个StringBuilder。因此,在简单情况下,声明自己的StringBuilder没有任何好处。但是,如果您正在构建大型的String对象,比如小于4k,则声明StringBuilder sb = StringBuilder(4096);比连接或使用默认构造函数要高效得多,该构造函数仅有16个字符。如果您的String将小于10k,则使用构造函数初始化为10k以确保安全。但是,如果它被初始化为10k,然后您写入超过10k的1个字符,它将被重新分配并复制到20k数组中。因此,高初始化比低初始化更好。
在自动调整大小的情况下,当第17个字符出现时,支持数组将被重新分配并复制为32个字符,在第33个字符出现时,这种情况再次发生,您需要将数组重新分配并复制到64个字符中。您可以看到这如何退化为大量的重新分配和复制,这正是您在第一次使用StringBuilder/Buffer时真正想避免的。
这是来自JDK 6抽象字符串生成器的源代码。
   void expandCapacity(int minimumCapacity) {
    int newCapacity = (value.length + 1) * 2;
        if (newCapacity < 0) {
            newCapacity = Integer.MAX_VALUE;
        } else if (minimumCapacity > newCapacity) {
        newCapacity = minimumCapacity;
    }
        value = Arrays.copyOf(value, newCapacity);
    }

最佳实践是在初始化StringBuilder/Buffer时,比你认为需要的稍微大一点,如果你不知道字符串的大小,但可以猜测。稍微多分配一些内存比多次重新分配和复制要好。
此外,要注意不要使用String来初始化StringBuilder/Buffer,因为这只会分配String的大小+16个字符的空间,这在大多数情况下只会开始退化的重新分配和复制循环,而这正是你想避免的。以下内容直接来自Java 6源代码。
public StringBuilder(String str) {
    super(str.length() + 16);
    append(str);
    }

如果你不小心得到了一个你没有创建并且无法控制构造函数的StringBuilder/Buffer实例,有一种方法可以避免退化的重新分配和复制行为。使用所需大小调用.ensureCapacity()以确保你的结果String适合。

其他选择:

顺便提一下,如果你正在进行非常繁重的String构建和操作,有一种更注重性能的替代方案叫做Ropes

另一个选择是通过子类化ArrayList<String>并添加计数器来跟踪每个.append()和其他列表变异操作上的字符数,然后重写.toString()以创建所需大小的StringBuilder,循环遍历列表并构建输出,甚至可以将该StringBuilder作为实例变量并“缓存”.toString()的结果,并且只有在发生更改时才需要重新生成它。
此外,在构建固定格式输出时,也不要忘记使用String.format(),编译器可以对其进行优化,使其更好。

1
String x = "A" + "B"; 真的会编译成 StringBuilder 吗?为什么不直接编译成 String x = "AB"; 呢?如果组件在编译时已知,它应该只使用 String 类型而不是 StringBuilder。 - Matt Greer
它可能会优化字符串常量,我记不清上一次反编译字节码时是否这样做了,但我知道如果其中有任何变量,它肯定会使用 StringBuilder 实现。您可以下载 JDK 源代码并自行查找。"A" + "B" 当然是一个人为的例子。 - user177800
我一直在想String.format()。我从未真正看到它在项目中被使用过。通常使用的是StringBuilder。嗯,通常实际上是"A" + "B" + "C"因为人们很懒;) 我倾向于始终使用StringBuilder,即使只有两个字符串被连接,因为将来可能会附加更多字符串。我从来没有使用过String.format(),主要是因为我从来没有记住它是在哪个JDK引入的-我看到它是JDK1.5,我会选择使用它而不是其他选项。 - jamiebarrow

10

您的意思是拼接字符串吗?

现实世界中的例子:您想从许多其他字符串中创建一个新字符串

例如,发送消息:

String

String s = "Dear " + user.name + "<br>" + 
" I saw your profile and got interested in you.<br>" +
" I'm  " + user.age + "yrs. old too"

可变字符串

String s = new StringBuilder().append.("Dear ").append( user.name ).append( "<br>" ) 
          .append(" I saw your profile and got interested in you.<br>") 
          .append(" I'm  " ).append( user.age ).append( "yrs. old too")
          .toString()
或者
String s = new StringBuilder(100).appe..... etc. ...
// The difference is a size of 100 will be allocated upfront as  fuzzy lollipop points out.

StringBuffer(语法与 StringBuilder 完全相同,但效果不同)

关于

StringBufferStringBuilder

前者是同步的,后者不是。

因此,如果您在单个线程中多次调用它(这在90%的情况下是如此),StringBuilder 将运行快得多,因为它不会停下来检查是否拥有线程锁。

因此,建议使用 StringBuilder(除非当然您有超过一个线程同时访问它,这是很少见的)

String 连接(使用 + 运算符)可能通过编译器优化为在底层使用 StringBuilder,所以不再需要担心,在Java早期,这是所有人都说应该尽量避免的事情,因为每个连接都创建了一个新的字符串对象。现代编译器不再这样做,但仍然最好使用 StringBuilder 以防您使用“旧”的编译器。

编辑

只是为了好奇的人,这就是编译器为这个类所做的。

class StringConcatenation {
    int x;
    String literal = "Value is" + x;
    String builder = new StringBuilder().append("Value is").append(x).toString();
}

使用javap -c命令查看StringConcatenation类的字节码。

Compiled from "StringConcatenation.java"
class StringConcatenation extends java.lang.Object{
int x;

java.lang.String literal;

java.lang.String builder;

StringConcatenation();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   aload_0
   5:   new #2; //class java/lang/StringBuilder
   8:   dup
   9:   invokespecial   #3; //Method java/lang/StringBuilder."<init>":()V
   12:  ldc #4; //String Value is
   14:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   17:  aload_0
   18:  getfield    #6; //Field x:I
   21:  invokevirtual   #7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   24:  invokevirtual   #8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   27:  putfield    #9; //Field literal:Ljava/lang/String;
   30:  aload_0
   31:  new #2; //class java/lang/StringBuilder
   34:  dup
   35:  invokespecial   #3; //Method java/lang/StringBuilder."<init>":()V
   38:  ldc #4; //String Value is
   40:  invokevirtual   #5; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   43:  aload_0
   44:  getfield    #6; //Field x:I
   47:  invokevirtual   #7; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   50:  invokevirtual   #8; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   53:  putfield    #10; //Field builder:Ljava/lang/String;
   56:  return

}

第5-27行是给名为"literal"的字符串使用的。

第31-53行是给名为"builder"的字符串使用的。

没有区别,两个字符串完全执行相同的代码。


2
这是一个非常糟糕的例子。使用“Dear”初始化StringBuilder意味着第一个.append()将导致重新分配和复制。完全抵消了您尝试通过“正常”连接获得的任何效率。更好的例子是使用初始大小创建它,该大小将容纳最终字符串的所有内容。 - user177800
1
通常使用StringBuilder在赋值右侧进行字符串连接不是一个好的编程实践。良好的实现将在后台使用StringBuilder,正如您所说的那样。此外,您的例子"a" + "b"将被编译为单个文字"ab",但如果您使用StringBuilder则会导致两次无用的append()调用。 - Mark Peters
@Mark,我不是想使用"a"+"b",而是想表达一下什么是字符串连接,所以我将其改为了显式的形式。你没有说的是,为什么这样做不是一个好习惯。这正是(现代)编译器所做的。@fuzzy,我同意,特别是当你知道最终字符串的大小(大约)时。 - OscarRyz
1
我不认为这是特别“糟糕”的做法,但我肯定不会推荐一般情况下这样做。这种写法很笨拙,难以阅读,而且过于冗长。此外,它还会鼓励像你这样的错误,其中你正在分解两个本来可以编译为一个的文字。只有在性能分析告诉我这样做有所改善时,我才会按照你的方式去做。 - Mark Peters
@Mark 我明白了。我想的更多是像模板一样的“大块代码”,而不是每个普通字符串字面量。但是,是的,我同意,由于它们现在执行相同的操作,这没有意义(10年前是拒绝代码修订更改的原因):) - OscarRyz

8

字符串家族

字符串

String类 表示字符序列。在Java程序中,所有的字符串字面值(例如"abc")都是该类的实例。

一旦创建了字符串对象,它们就是不可变的,我们无法更改它们。(字符串是常量

  • If a String is created using constructor or method then those strings will be stored in Heap Memory as well as SringConstantPool. But before saving in pool it invokes intern() method to check object availability with same content in pool using equals method. If String-copy is available in the Pool then returns the reference. Otherwise, String object is added to the pool and returns the reference.

    • The Java language provides special support for the string concatenation operator (+), and for conversion of other objects to strings. String concatenation is implemented through the StringBuilder(or StringBuffer) class and its append method.

    String heapSCP = new String("Yash");
    heapSCP.concat(".");
    heapSCP = heapSCP + "M";
    heapSCP = heapSCP + 777;
    
    // For Example: String Source Code 
    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);
    }
    
  • String literals are stored in StringConstantPool.

    String onlyPool = "Yash";
    

StringBuilderStringBuffer是可变的字符序列。这意味着您可以更改这些对象的值。StringBuffer具有与StringBuilder相同的方法,但是StringBuffer中的每个方法都已同步,因此它是线程安全的。

  • StringBuffer and StringBuilder data can only be created using new operator. So, they get stored in Heap memory.

  • Instances of StringBuilder are not safe for use by multiple threads. If such synchronization is required then it is recommended that StringBuffer be used.

    StringBuffer threadSafe = new StringBuffer("Yash");
    threadSafe.append(".M");
    threadSafe.toString();
    
    StringBuilder nonSync = new StringBuilder("Yash");
    nonSync.append(".M");
    nonSync.toString();
    
  • StringBuffer and StringBuilder are having a Special methods like., replace(int start, int end, String str) and reverse().

    NOTE: StringBuffer and SringBuilder are mutable as they provides the implementation of Appendable Interface.

何时使用哪个。

  • If a you are not going to change the value every time then its better to Use String Class. As part of Generics if you want to Sort Comparable<T> or compare a values then go for String Class.

    //ClassCastException: java.lang.StringBuffer cannot be cast to java.lang.Comparable
    Set<StringBuffer> set = new TreeSet<StringBuffer>();
    set.add( threadSafe );
    System.out.println("Set : "+ set);
    
  • If you are going to modify the value every time the go for StringBuilder which is faster than StringBuffer. If multiple threads are modifying the value the go for StringBuffer.


8
--------------------------------------------------------------
                字符串               StringBuffer         StringBuilder
--------------------------------------------------------------                
存储区     |  常量字符串池            堆                    堆 
可修改性   |  否(不可变)           是(可变)           是(可变)
线程安全性 |  是                       是                      否
性能       |  快                   非常慢                快
--------------------------------------------------------------

为什么String的性能很快,而StringBuffer的性能非常慢? - Gaurav
1
@gaurav,你可以阅读它的源代码,StringBuffer 中的所有方法都是“同步”的,这就是原因。 - Hearen
字符串的存储区域取决于我们创建字符串的方式,比如说- 方法1:String str1= "ABC" - 值将存储在SCP中(它位于堆内存中)方法2:String str1= new String("ABC")- 对象将在堆内存中创建。 - Vipul Gupta

4
此外,StringBuffer 是线程安全的,而 StringBuilder 则不是。
因此,在多个线程同时访问的实时情况下,StringBuilder 可能会产生不确定的结果。

3
请注意,如果您使用的是Java 5或更高版本,则应该使用StringBuilder而不是StringBuffer。根据API文档:
自JDK 5发布以来,这个类已经增加了一个等效的类,专为单个线程设计,即StringBuilder。通常应该优先使用StringBuilder类,因为它支持所有相同的操作,但它速度更快,因为它不执行同步。
实际上,您几乎永远不会同时从多个线程中使用它,因此StringBuffer执行的同步几乎总是不必要的开销。

3

个人而言,我认为StringBuffer没有任何实际的应用场景。我什么时候需要通过操作字符序列在多个线程之间进行通信呢?这听起来一点也不有用,但也许我还没有看到光明 :)


3
String和另外两个类的区别在于,String是不可变的,而另外两个类是可变的。

但是为什么我们有两个用途相同的类呢?

原因是StringBuffer是线程安全的,而StringBuilder则不是。 StringBuilder是StringBuffer Api中的一个新类,在JDK5中引入,如果您在单线程环境中工作,则始终建议使用它,因为它更快速。

有关完整详情,请阅读http://www.codingeek.com/java/stringbuilder-and-stringbuffer-a-way-to-create-mutable-strings-in-java/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接