StringBuilder与String:考虑替换的区别

35

在拼接大量字符串时,我被建议使用StringBuilder来做如下操作:

StringBuilder someString = new StringBuilder("abc");
someString.append("def");
someString.append("123");
someString.append("moreStuff");

相对于

String someString = "abc";
someString = someString + "def";
someString = someString + "123";
someString = someString + "moreStuff";

这将导致创建多个字符串,而不是一个。

现在,我需要做类似的事情,但是我要使用String的replace方法,如下所示:

String someString = SOME_LARGE_STRING_CONSTANT;
someString = someString.replace("$VARIABLE1", "abc");
someString = someString.replace("$VARIABLE2", "def");
someString = someString.replace("$VARIABLE3", "123");
someString = someString.replace("$VARIABLE4", "moreStuff");

要使用StringBuilder实现相同的功能,我需要这样做,仅适用于一个替换操作:

someString.replace(someString.indexOf("$VARIABLE1"), someString.indexOf("$VARIABLE1")+10, "abc");

我的问题是:"是使用String.replace并创建大量额外的字符串更好,还是仍然使用StringBuilder,并拥有像上面那样冗长的行?"


如果出现性能问题,请更改它。如果没有更重要的更改需要执行,请更改它。如果输入非常大且经常使用,请更改它。顺便说一句,第二种方法不起作用,因为它只会替换一次,您必须将其放在while循环中。请查看我的答案。 - OscarRyz
9个回答

47

StringBuilder比手动连接或修改字符串更好,因为StringBuilder是可变的,而String是不可变的,每次修改都需要创建一个新的String。

需要注意的是,Java编译器将自动转换以下示例:

String result = someString + someOtherString + anotherString;

转化为类似以下的内容:

String result = new StringBuilder().append(someString).append(someOtherString).append(anotherString).toString();

尽管如此,除非你要替换大量字符串,选择更易读和可维护的方法。如果你可以通过一系列'替换'调用使代码更干净,请使用该方法,而不是使用StringBuilder方法。与应对微小优化的悲惨悲剧所节省的精力相比,差异将是微不足道的。

附言

对于您的代码示例(正如OscarRyz指出的那样,如果您在'someString'中有多个"$VARIABLE1",则该示例将不起作用,这种情况下您需要使用循环),您可以在以下位置缓存indexOf调用的结果:

someString.replace(someString.indexOf("$VARIABLE1"), someString.indexOf("$VARIABLE1")+10, "abc");

使用

int index = someString.indexOf("$VARIABLE1");    
someString.replace(index, index+10, "abc");

无需重复搜索字符串 :-)


如果输入中有两个$VARIABLE1,这将失败(它只替换第一个)。你必须将其放在while循环中。 - OscarRyz
@OscarRyz 哎呀!没错。我甚至没有注意到那个问题:-p。我写这段代码的主要原因仅仅是为了提醒问答者他们不应该调用两次 indexOf。 - Zach L
顺便说一句,这是一个好观点。我在示例中尝试了同样的方法,直到那时才意识到。很容易忘记。请查看我的答案。 - OscarRyz
@OscarRyz @ZachL:示例:builder = new StringBuilder(120);builder.append(a()).append(b()).append(c());嗨,如果我们知道输出字符串最终大小,比如说是120,那么上述代码不会比编译过程中自动生成的代码更好吗?请注意,a、b、c方法不返回静态硬编码的字符串,因此编译器不知道要用什么值初始化构建器。 - saurabheights

8

你知道吗?如果你正在使用Java 1.5+,那么字符串字面量的连接方式与以前相同。

  String h = "hello" + "world";

并且

  String i = new StringBuilder().append("hello").append("world").toString();

这些是相同的。

所以,编译器已经为您完成了工作。

当然,更好的做法是:

 String j = "hellworld"; // ;) 

关于第二个问题,是的,那是更好的选择,但用"搜索和替换"以及一些正则表达式技巧应该不难实现。

例如,您可以定义一个像这个示例中的方法:

  public static void replace( String target, String replacement, 
                              StringBuilder builder ) { 
    int indexOfTarget = -1;
    while( ( indexOfTarget = builder.indexOf( target ) ) >= 0 ) { 
      builder.replace( indexOfTarget, indexOfTarget + target.length() , replacement );
    }
  }

您的代码目前看起来像这样:

someString = someString.replace("VARIABLE1", "abc");
someString = someString.replace("VARIABLE2", "xyz");

你只需要使用文本编辑器并触发类似于这样的vi搜索和替换命令即可:

(您只需使用文本编辑器,然后触发类似于以下内容的vi搜索和替换命令)

%s/^.*("\(.*\)".\s"\(.*\)");/replace("\1","\2",builder);

这句话的意思是:“取括号内看起来像字符串字面量的任何内容,并将其放入另一个字符串中”。

那么你的代码将从这个样子变为:

someString = someString.replace("VARIABLE1", "abc");
someString = someString.replace("VARIABLE2", "xyz");

转换为:

replace( "VARIABLE1", "abc", builder );
replace( "VARIABLE2", "xyz", builder );

很快就完成了。

这是一个可工作的演示:

class DoReplace { 
  public static void main( String ... args ) {
    StringBuilder builder = new StringBuilder(
       "LONG CONSTANT WITH VARIABLE1 and  VARIABLE2 and VARIABLE1 and VARIABLE2");
    replace( "VARIABLE1", "abc", builder );
    replace( "VARIABLE2", "xyz", builder );
    System.out.println( builder.toString() );
  }
  public static void replace( String target, String replacement, 
                              StringBuilder builder ) { 
    int indexOfTarget = -1;
    while( ( indexOfTarget = builder.indexOf( target ) ) > 0 ) { 
      builder.replace( indexOfTarget, indexOfTarget + target.length() , 
                       replacement );
    }
  }
}

如果目标实例从位置0开始,会发生什么?你的while循环退出条件表明它不会替换它。 - Matthew Cox
2
实际上,在您的第一个示例中,编译器实际上并没有生成StringBuilder解决方案,而是生成了第三行代码,因为它是一个编译时常量。因此这些是等价的。 - Paŭlo Ebermann

3
我会建议使用StringBuilder,然后编写一个包装器来方便代码更加易读和易于维护,同时仍然保持效率。 =D
import java.lang.StringBuilder;
public class MyStringBuilder
{
    StringBuilder sb;

    public MyStringBuilder() 
    {
       sb = new StringBuilder();
    }

    public void replace(String oldStr, String newStr)
    {
            int start = -1;
            while ((start = sb.indexOf(oldStr)) > -1)
            {
                    int end = start + oldStr.length(); 
                    sb.replace(start, end, newStr);
            }
    }

    public void append(String str)
    {
       sb.append(str);
    }

    public String toString()
    {
          return sb.toString();
    }

    //.... other exposed methods

    public static void main(String[] args)
    {
          MyStringBuilder sb = new MyStringBuilder();
          sb.append("old old olD dudely dowrite == pwn");
          sb.replace("old", "new");
          System.out.println(sb);
    }
}

输出:

new new olD dudely dowrite == pwn

现在你只需要使用新版本,它只有一行简单的命令。
MyStringBuilder mySB = new MyStringBuilder();
mySB.append("old dudley dowrite == pwn");
mySB.replace("old", "new"):

输入“old old dudley”失败。 - OscarRyz
@OscarRyz 有趣。我在一个项目中使用它,输出结果正是预期的。请注意,我没有添加append(String)或String toString()方法。我决定添加它们以增强代码的清晰度。 - Matthew Cox
可能你从未需要替换超过一个字符串。试试使用“old old dudley”,你会得到“new old dudley”。 - OscarRyz
@OscarRyz 哈哈,抱歉了。我忘记它需要替换旧字符串的所有实例。已经进行了更正。 - Matthew Cox

1

不必像那样写长行,你可以编写一个用于替换 StringBuilder 字符串部分的方法,就像这样:

public StringBuilder replace(StringBuilder someString, String replaceWhat, String replaceWith) {
   return someString.replace(someString.indexOf(replaceWhat), someString.indexOf(replaceWhat)+replaceWhat.length(), replaceWith);
}

我也在考虑同样的事情,但这需要一个while循环来替换所有的内容,而不仅仅是第一次出现的。 - OscarRyz

0
也许String类在内部使用indexOf方法来查找旧字符串的索引并替换为新字符串。
而且StringBuilder不是线程安全的,所以执行速度更快。

0
如果你的字符串确实很大,而且你担心性能问题,我建议编写一个类,它接受你的模板文本和变量列表,然后逐个字符地读取源字符串并使用 StringBuilder 构建结果。从 CPU 和内存使用方面来看,这应该是最有效的方法。此外,如果你从文件中读取这个模板文本,我不会一次全部加载到内存中。当你从文件中读取它时,分块处理。
如果你只是想找一种构建字符串的好方法,它比重复附加字符串更有效率,你可以使用String.format()。它的工作方式类似于 C 语言中的 sprintf()。MessageFormat.format() 也是一个选项,但它使用 StringBuffer。

这里还有一个相关的问题:如何在不使用连接符的情况下将Java字符串插入另一个字符串中?


0
所有人的代码都有一个bug。尝试使用yourReplace("x","xy")。它会无限循环。

我已经制作了这个包装器,没有那个问题 https://gist.github.com/ipoletti/c58902bb9571c8cdc527 - Ignacio A. Poletti

0

虽然微小的优化可能会带来问题,但有时取决于上下文。例如,如果您的替换发生在具有10000次迭代的循环内部,那么您将从“无用”的优化中看到显着的性能差异。

然而,在大多数情况下,最好以可读性为重。


0
Jam Hong 是正确的 - 上述解决方案都存在无限循环的潜在可能性。我想要从中吸取的教训是,微小的优化通常会引起种种可怕的问题,并且并没有真正节省太多时间。尽管如此,这里有一个不会无限循环的解决方案。
private static void replaceAll(StringBuilder builder, String replaceWhat, String replaceWith){
    int occuranceIndex = builder.indexOf(replaceWhat);
    int lastReplace = -1;
    while(occuranceIndex >= 0){
        if(occuranceIndex >= lastReplace){
            builder.replace(occuranceIndex, occuranceIndex+replaceWhat.length(), replaceWith);
            lastReplace = occuranceIndex + replaceWith.length();
            occuranceIndex = builder.indexOf(replaceWhat);
        }else{
            break;
        }
    }
}

这就像是StringBuilder的replaceFirst。 StringBuilder builder = new StringBuilder("Var x and x and x and x"); replaceAll(builder, "x", "xy"); - irmakoz

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