有人告诉我,StringBuffer
在 Java 中使用连接字符串比使用 s 的+
运算符更有效String
。当你这样做时,引擎盖下会发生什么?有什么StringBuffer
不同?
19 回答
现在,几乎在所有情况下,最好使用 StringBuilder (它是一个不同步的版本;你什么时候并行构建字符串?),但会发生以下情况:
当您将 + 与两个字符串一起使用时,它会编译如下代码:
String third = first + second;
对于这样的事情:
StringBuilder builder = new StringBuilder( first );
builder.append( second );
third = builder.toString();
因此,对于一些小例子,它通常没有什么区别。但是,当您构建一个复杂的字符串时,您通常需要处理的事情远不止这些;例如,您可能正在使用许多不同的附加语句,或者像这样的循环:
for( String str : strings ) {
out += str;
}
在这种情况下,每次迭代都需要一个新的StringBuilder
实例和一个新的( -sString
的新值是不可变的)。这是非常浪费的。用一个来替换它意味着你可以只生产一个,而不是用你不关心的 s来填充堆。out
String
StringBuilder
String
String
对于简单的连接,例如:
String s = "a" + "b" + "c";
使用它是毫无意义的StringBuffer
——正如jodonnell指出的那样,它将被巧妙地翻译成:
String s = new StringBuffer().append("a").append("b").append("c").toString();
但是在循环中连接字符串是非常糟糕的,例如:
String s = "";
for (int i = 0; i < 10; i++) {
s = s + Integer.toString(i);
}
在此循环中使用字符串会在内存中生成 10 个中间字符串对象:“0”、“01”、“012”等。在使用您编写相同的内容时,StringBuffer
您只需更新一些内部缓冲区,StringBuffer
并且您不会创建那些您不需要的中间字符串对象:
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10; i++) {
sb.append(i);
}
实际上,对于上面的示例,您应该使用StringBuilder
(在 Java 1.5 中引入)而不是StringBuffer
-StringBuffer
稍微重一些,因为它的所有方法都是同步的。
一个不应该比另一个快。在 Java 1.4.2 之前情况并非如此,因为当使用“+”运算符连接两个以上的字符串时,String
会在构建最终字符串的过程中创建中间对象。
但是,正如StringBuffer 的 JavaDoc所述,至少从使用“+”运算符的 Java 1.4.2 开始编译为创建 aStringBuffer
和append()
ing 到它的许多字符串。所以显然没有区别。
但是,在循环内使用将字符串添加到另一个字符串时要小心!例如:
String myString = "";
for (String s : listOfStrings) {
// Be careful! You're creating one intermediate String object
// for every iteration on the list (this is costly!)
myString += s;
}
但是请记住,通常将几个字符串与“+”连接比append()
将它们全部连接起来更干净。
在后台,它实际上创建并附加到一个 StringBuffer,在结果上调用 toString()。因此,实际上您不再使用哪个并不重要。
所以
String s = "a" + "b" + "c";
变成
String s = new StringBuffer().append("a").append("b").append("c").toString();
这对于单个语句中的一堆内联追加是正确的。如果您在多个语句的过程中构建字符串,那么您就是在浪费内存,而 StringBuffer 或 StringBuilder 是您更好的选择。
我认为给定 jdk1.5(或更高版本)并且您的连接是线程安全的,您应该使用 StringBuilder 而不是 StringBuffer http://java4ever.blogspot.com/2007/03/string-vs-stringbuffer-vs-stringbuilder.html 至于速度上的提升: http ://www.about280.com/stringtest.html
就我个人而言,我会编写代码以提高可读性,因此除非您发现字符串连接会使您的代码变得相当慢,否则请使用使您的代码更具可读性的任何方法。
在某些情况下,由于编译器执行的优化,这已过时,但一般问题是代码如下:
string myString="";
for(int i=0;i<x;i++)
{
myString += "x";
}
将如下所示(每个步骤都是下一个循环迭代):
- 构造一个长度为 1 且值为 "x" 的字符串对象
- 创建一个大小为 2 的新字符串对象,将旧字符串“x”复制到其中,在位置 2 添加“x”。
- 创建一个大小为 3 的新字符串对象,将旧字符串“xx”复制到其中,在位置 3 添加“x”。
- ... 等等
如您所见,每次迭代都必须再复制一个字符,导致我们在每个循环中执行 1+2+3+4+5+...+N 次操作。这是一个 O(n^2) 操作。但是,如果我们事先知道我们只需要 N 个字符,我们可以在一次分配中完成,只复制我们正在使用的字符串中的 N 个字符 - 只需 O(n) 操作。
StringBuffer/StringBuilder 避免了这种情况,因为它们是可变的,因此不需要一遍又一遍地复制相同的数据(只要在它们的内部缓冲区中有空间可以复制)。他们避免执行与附加数量成正比的分配和复制,方法是按照其当前大小的比例过度分配缓冲区,从而提供摊销的 O(1) 附加。
然而值得注意的是,编译器通常能够自动将代码优化为 StringBuilder 样式(或者更好——因为它可以执行常量折叠等)。
AFAIK 它取决于 JVM 的版本,在 1.5 之前的版本中,使用“+”或“+=”实际上每次都复制整个字符串。
请注意,使用 += 实际上会分配字符串的新副本。
正如在循环中使用 + 所指出的那样,涉及复制。
当连接的字符串是编译时常量时,在编译时连接,所以
String foo = "a" + "b" + "c";
已编译为:
String foo = "abc";
Java 将 string1 + string2 转换为 StringBuffer 构造、append() 和 toString()。这是有道理的。
但是,在 Java 1.4 和更早版本中,它会为语句中的每个+ 运算符单独执行此操作。这意味着执行 a + b + c 将导致两个带有两个toString() 调用的StringBuffer 构造。如果你有一长串 concats,它会变成一团糟。自己做意味着你可以控制它并正确地做到这一点。
Java 5.0 及更高版本似乎更明智地做到了这一点,因此问题更小,当然也更少冗长。
更多信息:
StringBuffer 是一个线程安全的类
public final class StringBuffer extends AbstractStringBuilder
implements Serializable, CharSequence
{
// .. skip ..
public synchronized StringBuffer append(StringBuffer stringbuffer)
{
super.append(stringbuffer);
return this;
}
// .. skip ..
}
但是 StringBuilder 不是线程安全的,因此如果可能的话使用 StringBuilder 会更快
public final class StringBuilder extends AbstractStringBuilder
implements Serializable, CharSequence
{
// .. skip ..
public StringBuilder append(String s)
{
super.append(s);
return this;
}
// .. skip ..
}
StringBuffer 类维护一个字符数组来保存您连接的字符串的内容,而 + 方法在每次调用时创建一个新字符串并附加两个参数 (param1 + param2)。
StringBuffer 更快,因为 1. 它可能能够使用其已经存在的数组来连接/存储所有字符串。2. 即使它们不适合数组,分配更大的后备数组比为每次调用生成新的 String 对象更快。
StringBuffer 是可变的。它将字符串的值添加到同一个对象而不实例化另一个对象。做类似的事情:
myString = myString + "XYZ"
将创建一个新的String 对象。
要使用“+”连接两个字符串,需要为两个字符串分配空间,然后从两个字符串复制数据。StringBuffer 针对连接进行了优化,并分配了比最初所需更多的空间。在连接新字符串时,在大多数情况下,字符可以简单地复制到现有字符串缓冲区的末尾。
对于连接两个字符串,'+' 运算符的开销可能会更少,但是当你连接更多的字符串时,StringBuffer 会领先,使用更少的内存分配和更少的数据复制。
因为字符串是不可变的,所以每次调用 + 运算符都会创建一个新的 String 对象并将 String 数据复制到新的 String 中。由于复制字符串所需的时间与字符串的长度呈线性关系,因此对 + 运算符的 N 次调用序列会导致 O(N 2 ) 运行时间(二次)。
相反,由于 StringBuffer 是可变的,它不需要每次执行 Append() 时都复制 String,因此 N Append() 调用的序列需要 O(N) 时间(线性)。如果您将大量字符串附加在一起,这只会在运行时产生显着差异。
如前所述,String 对象是不可变的,这意味着一旦创建(见下文)就无法更改。
String x = new String("something"); // 或者
字符串 x = "某事";
因此,当您尝试连接 String 对象时,会获取这些对象的值并将其放入新的 String 对象中。
如果您改为使用可变的 StringBuffer,您会不断地将值添加到 char(基元)的内部列表中,该列表可以扩展或截断以适应所需的值。不创建新对象,仅在需要保存值时创建/删除新字符。
连接两个字符串时,实际上是在 Java 中创建了第三个 String 对象。使用 StringBuffer(或 Java 5/6 中的 StringBuilder)更快,因为它使用内部字符数组来存储字符串,并且当您使用它的 add(...) 方法之一时,它不会创建新的 String目的。相反,StringBuffer/Buider 附加了内部数组。
在简单的连接中,使用 StringBuffer/Builder 或 '+' 运算符连接字符串并不是真正的问题,但是在进行大量字符串连接时,您会发现使用 StringBuffer/Builder 更快。
原因是字符串不可变。它不是修改字符串,而是创建一个新字符串。字符串池存储所有字符串值,直到垃圾收集器对其进行处理。想想有两个字符串作为Hello
和how are you
。如果我们考虑字符串池,它有两个字符串。
如果您尝试将这两个字符串连接为,
字符串 1 = 字符串 1+字符串 2
现在创建一个新的 String 对象并将其存储在 String 池中。
如果我们尝试连接数千个单词,它会获得更多内存。解决方案是 StringBuilder 或 StringBuffer。它只能创建一个 Object 并且可以修改。因为两者都是可变的。那么不需要更多的内存。如果您认为线程安全,则使用 StringBuffer,否则使用 StringBuilder。
public class StringExample {
public static void main(String args[]) {
String arr[] = {"private", "default", "protected", "public"};
StringBuilder sb= new StringBuilder();
for (String value : arr) {
sb.append(value).append(" ");
}
System.out.println(sb);
}
}
输出:私有默认受保护公共
因为字符串在 Java 中是不可变的,所以每次连接字符串时,都会在内存中创建新对象。SpringBuffer 在内存中使用相同的对象。
我认为最简单的答案是:它更快。
如果您真的想了解所有底层知识,您可以随时查看源代码:
Java 语言规范的字符串连接运算符 +部分为您提供了更多关于 + 运算符为何如此缓慢的背景信息。