使用私有方法返回字符串与传递StringBuilder相比,有哪些优缺点?

6
考虑一个有“buildMessage”方法的类(类似于):
public class MessageBuilder {
  public String buildMessage() {
    //Build up the message and return
    //
    //Get the header
    //Get the body
    //Get the footer
    //return
  }
}

当我们构建消息时,最好使用StringBuilder(或类似的缓冲对象)来构建字符串,而不是仅仅将一堆字符串连接在一起。但如果返回String而不是将您的StringBuilder作为参数传递,那么是否会失去这种优势呢?

换句话说,这样写很清晰易懂:

private String getHeader() {
  StringBuilder builder = new StringBuilder();
  builder.append("Hello ")
     .append(this.firstname)
     .append(",\n");
  return builder.toString();
}

对我来说,这比强制使用StringBuilder更自然,但我们也可以这样写:

private void appendHeader(StringBuilder builder) {
   builder.append("Hello ")
      .append(this.firstname)
      .append(",\n");
}

第一个选项可以让我们使用“get”方法,即使意图并非将返回值附加到缓冲区中。这也使得公共方法易于理解。
public class MessageBuilder {
  public String buildMessage() {
    StringBuilder builder = new StringBuilder();
    builder.append(getHeader())
       .append(getBody())
       .append(getFooter());
    return builder.toString();
  }
}

使用第二个选项会导致:
public class MessageBuilder {
  public String buildMessage() {
    StringBuilder builder = new StringBuilder();
    appendHeader(builder);
    appendBody(builder);
    appendFooter(builder);
    return builder.toString();
  }
}

我的问题是,第一种选项是否存在与“连接字符串”相同的内存问题。我对哪一种更易读感兴趣(因为如果有清晰的胜者以方便阅读,那么这将会是一个重要的考虑因素),但我也对效率很感兴趣。我怀疑它们几乎没有区别,但想知道每种方法所涉及的成本 - 如果您了解,请分享!

@Ed Staub,StringBuilder 不是线程安全的。Todd R 可以安全地并行运行不同的 getHeader、getBody 和 getFooter 部分。但如果他尝试并行运行 appendHeader、appendBody 和 appendFooter,事情就会混乱。这不是一个重要的问题,因为他不太可能尝试并行处理。 - emory
@emory - 我不同意,因为构建器在每个线程中作为本地变量传递 - 它从未在线程之间共享。除非附加方法由于某些其他原因是线程不安全的,否则它是可以的。如果它们由于某些其他原因不安全,则两种解决方案都同样适用。 - Ed Staub
@Ed Staub - 您是正确的 - 这两种方法都是线程安全的。我撤回我的观点。 - emory
5个回答

1

重复使用 StringBuilder 在 CPU 方面更有效率。然而,我怀疑这并不会真正有所影响。

你应该选择你认为最自然和最清晰的方法。(从你的说法来看,似乎是使用 StringBuilder)

仅凭性能往往不是做某事的好理由,这比许多人想象的要少。


就对象创建而言,关于CPU方面的观点是一致的。但是,如果您进行了大量不必要和/或浪费的字符串连接,则垃圾回收的CPU使用情况可能会成为一个大问题。从这个角度来看,我更喜欢聪明地使用StringBuilder和/或StringBuffer。 - Chris Aldrich
我喜欢自己编写无GC代码。我会使用ThreadLocal<ByteBuffer>,并且不创建任何对象,即使是包含char[]的最终字符串。但这并非适用于每个人。 ;) - Peter Lawrey
@Peter,记住如果像上面那样的代码被用在一个被数百万用户同时访问的网页中,性能是非常重要的。如果不是玩具程序,使用“+”是一个非常非常糟糕的习惯。 - Angel O'Sphere
经常使用 + 是不好的。偶尔使用可能没有问题。如果它只在 buildMessage() 中使用了两次,那么可能还好。但是在构建正文时使用它更可能会导致一些灾难,因为它可能被频繁调用。 - Peter Lawrey

0

一厢情愿的想法

如果StringBuilder不是一个final类,那就太好了。将append...方法放在一个继承StringBuilder的内部类中:

public class MessageBuilder {

  public String buildMessage() {
    return "" + MyStringBuilder()
        .appendHeader()
        .appendBody()
        .appendFooter();
  }

  private class MyBuilder extends StringBuilder
  {
    MyBuilder appendHeader() {
         append("Hello ")
        .append(this.firstname)
        .append(",\n");
  }
}

一种实际可行的方法 - 需要更多的工作 - 是编写一个(非最终!)StringBuilder代理类,并将此类特殊扩展实现为其子类。有关详细讨论,请参见此处

0
只是因为我在其他答案中没有清楚地看到它提到:
private String getHeader() {
  StringBuilder builder = new StringBuilder();
  builder.append("Hello ")
     .append(this.firstname)
     .append(",\n");
  return builder.toString();
}

可以被替换为

private StringBuilder builder = new StringBuilder();

private String getHeader() {
  builder.setLength(0);
  builder.append("Hello ")
     .append(this.firstname)
     .append(",\n");
  return builder.toString();
}

缓冲区将被分配一次。当缓冲区被扩展时,扩展将保留直到声明对象被垃圾回收。因此,不会浪费重新分配和调整缓冲区的时间。

唯一的问题是您可能需要使您的方法同步(即线程安全)。

使用现有的字符串缓冲区比使用字符串更快,因为不需要分配。手动连接字符串相当昂贵,因为通常需要多次分配。

P.S.:当然,字符串构建器的私有分配可以在私有方法之间共享,这意味着最终用户不需要自己分配它。在某些情况下,传递缓冲区使代码更易于阅读和更具功能性。这里没有明确的答案或规则,这真的取决于您正在做什么以及您需要什么。


如果您是多线程的,则可以声明一个ThreadLocal StringBuilder实例,或者使用StringBuffer,因为它在并发访问方面效果更好。 - Chris Aldrich
@Chris 这就是让用户提供字符串缓冲区更好的选择,因为这样就不需要同步。 - Jérôme Verstrynge
你写的代码完全符合我描述的要求,唯一的区别是你没有将它封装在一个类中。干杯! - Erx_VB.NExT.Coder
...以及重新调整/重新分配的动机和必要性,将长度设置为0,线程安全讨论以及可读性讨论。 - Jérôme Verstrynge
@jverstry 我只是在提到你的代码,而不是讨论部分,我喜欢你所创建的讨论话题,并因此为你投了赞成票。 - Erx_VB.NExT.Coder

0

基本上,使用第一种选项,您会遭受两种不同的低效率。

1). 您将创建一个stringBuilder对象3次(总共4次),而在第二个选项中,它只创建一次并重复使用。创建对象必须是值得的,因此通常在对字符串执行多个字符串操作任务时使用stringBuilder。

2). 因为每个标题、正文和页脚都返回一个字符串,在每种情况下,您都会创建一个普通字符串并不必要地复制它,这违反了使用stringBuilder的目的。基本上,连接普通字符串只是从头开始重新创建它,因此当您创建时,您会遭受与连接相同的开销(因为连接也只是从头开始重新创建)。

您最好创建一个类,在类的构造函数中可以创建您的stringBuilder对象,因此您不需要在上层代码中执行此操作。在类内部,您可以拥有三种方法,每种方法在构建并最终返回字符串时都使用私有stringBuilder对象。这样,您可以直接进行调用,而无需传递stringBuilder参数或在上层代码中创建对象(根据您的代码自动处理混乱的内容,这将使其更易于阅读)... :)

希望这能有所帮助。如果你需要我进一步解释,请告诉我。

另外,只有在服务器上调用这些过程的次数很多,特别是在 For 循环中发生时,你才应该担心这些问题,尤其是两者都适用的情况 :)。

祝你好运。


-1

如果“StringBuilder”被隐藏在实例变量中,最后一种方法将是真正的构建器模式。 使用该设计模式(第一个示例不是构建器设计模式)的原因是能够创建派生类,覆盖appendHeader / appendFooter和appendBody方法(例如,您想要构建小的HTML片段)。 如果您只想优雅地构造字符串,则流畅的示例(倒数第二个示例)看起来最好,并且可能对年轻程序员来说最容易理解。


啊,有些不知所措的人甚至没有评论就给了我-1。很好。 - Angel O'Sphere

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