使用StringBuilder与StringWriter和PrintWriter组装字符串的区别

63

最近我遇到了一个我以前没有见过的习语:使用StringWriterPrintWriter进行字符串拼接。我的意思是,我知道如何使用它们,但我一直在使用StringBuilder。是否有明确的理由来偏爱其中之一?StringBuilder方法对我来说更自然,但这只是风格上的区别吗?

我查看了几个问题(包括这个问题:StringWriter或StringBuilder),但没有一个回答实际上涉及到是否有理由为简单的字符串拼接而偏好其中之一。

这是我已经看到并且使用了很多次的习惯用法:使用StringBuilder进行字符串拼接:

public static String newline = System.getProperty("line.separator");
public String viaStringBuilder () {
   StringBuilder builder = new StringBuilder();
   builder.append("first thing").append(newline);  // NOTE: avoid doing builder.append("first thing" + newline);
   builder.append("second thing").append(newline);
   // ... several things
   builder.append("last thing").append(newline);
   return builder.toString();
}

这是一种新的惯用法:使用StringWriter和PrintWriter进行字符串拼接:

public String viaWriters() {
   StringWriter stringWriter = new StringWriter();
   PrintWriter printWriter = new PrintWriter(stringWriter);
   printWriter.println("first thing");
   printWriter.println("second thing");
   // ... several things
   printWriter.println("last thing");
   printWriter.flush();
   printWriter.close();
   return stringWriter.toString();
}

编辑 看起来没有一个具体的理由可以优先选择其中一个,因此我接受了最符合我的理解的答案,并点赞了所有其他答案。此外,我还发布了自己的答案,介绍了我运行基准测试的结果,以回应其中一个答案。谢谢大家。

再次编辑 结果发现有一个具体的理由要优先选择一个(具体来说是StringBuilder)。我第一次错过的是添加换行符。当您添加换行符(如上所述,作为单独的附加内容)时,速度略快-不是非常快,但与意图的清晰度相结合,它绝对更好。请参见下面的答案以获得改进后的时间记录。


6
不应使用builder.append("first thing" + newline)。问题在于它多了一个不必要的String分配(首先创建“first thing\n”,然后将字符串数据复制到builder)。相反,应该这样做:builder.append("first thing").append(newline)。这直接将“first thing”复制到builder,然后是newline。当然,只有当物品数量不固定(例如在循环中)时才应使用StringBuilder,javac已经将"a" + "b" + "c"优化为new StringBuilder().append("a").append("b").append("c").toString() - robinst
@robinst 抱歉回复很慢。我刚刚重新查看了这个问题并注意到了你的评论。非常好的观点,我已经重新进行了测试。请查看编辑。 - CPerkins
5
Throwable.printStackTrace() 需要一个写入器。 - ceving
1
我很惊讶OP出于"格式"原因接受了答案,Alan Moore的解释已经足够了。我曾经遇到过这样一种情况,因为API(第三方API)期望Writer实例,所以我被迫使用StringWriter,而我只想将内容写入字符串而不是文件/标准输出。只要writer没有被多个线程共享,StringWriter在内部使用StringBuffer/StringBuilder其实并不重要。 - srikanth yaradla
7个回答

43

StringWriter用于将内容写入字符串,但是当你需要使用一个期望WriterStream的API时,你可以使用它。它不是一种替代方法,而是一种妥协:只在必要时使用StringWriter


谢谢,这确实有道理,但我发现这在一个由一组非常聪明的人编写的代码库中被广泛使用 - 实际上,平均而言,他们似乎是我曾经合作过的最聪明的人。因此,我正在寻找他们可能这样做的原因。 - CPerkins
3
这应该是针对这个问题的接受答案。我曾经遇到过这样的情况,因为 API(第三方 API)需要 Writer 实例,而我只想将内容写入字符串,而不是文件或标准输出,所以我被迫使用 StringWriter。Alan 在他说“当需要时使用 StringWriter”时说得非常准确。 - srikanth yaradla
2
我遇到了这样一种情况,不得不使用 StringWriter,因为 java.lang.Throwable 类的 API 没有任何方法接收 StringBuilder 作为参数,当需要打印异常的堆栈跟踪时(只有接受 PrintStreamPrintWriter 的方法)。 - user2027342
要明确一点,这是一个妥协,因为您想使用 StringBuilder(假设没有多个线程涉及),这将更加清洁和高效。 - sactiw

22
样式上,StringBuilder的方法更干净。它的代码行数更少,并且使用的类专门设计用于构建字符串。

另一个考虑因素是哪个更高效。正确的回答应该是测试这些替代方案,但(根据源代码),我期望StringBuilder会更快一些。

StringWriter使用底层的StringBuffer(而不是StringBuilder)来保存写入“流”的字符。因此,使用StringWriter进行字符串组装将会产生StringBuffer的同步开销,无论您的应用程序是否需要它。但是,如果您的应用程序确实需要同步,则应考虑直接使用StringBuffer


CPerkins进行了一些基准测试(请参见他的答案),他的结果与我的直觉相符。他们说,最佳 StringBuilder StringWriter 之间的差异约为5%,这可能对于典型应用程序来说是微不足道的1。然而,知道“风格”和“性能”标准给出了相同的答案是很好的! < p> 1-虽然典型的应用程序不会花费大部分时间组装字符串,但也有例外情况。 但是,不应仅基于猜测就花时间优化这种事情。 首先对代码进行配置文件。

21

好的,鉴于其他答案似乎强调了更倾向于一种方法的风格原因,我决定进行一个时间测试。

编辑:根据robinst上面的评论,现在我有三种方法:一种是使用PrintWriter(StringWriter)-style追加,另外两种使用StringBuilder:一种是在append(与原来相同:builder.append(thing + newline);)内部嵌入换行符,另一种则是分别附加(如上所述:builder.append(thing).append(newline);)。

我按照我的惯例进行基准测试:首先调用这些方法多个(此处为1000)次,以便优化器有足够的时间预热;然后交替地多次(此处为100,000)调用每个方法,以便工作站运行时环境上的任何变化更加公平;最后多次运行测试并平均结果。

当然,每对调用创建的行数和行长度都是随机的,但保持相同,因为我想避免缓冲区大小或行大小对结果产生任何可能的影响。

这里是此代码:http://pastebin.com/vtZFzuds

注意:我还没有更新pastebin代码以反映新的测试。

简而言之?每种方法的100,000次调用的平均结果非常接近:

  • 使用StringBuilder(括号内+换行符)每次调用0.11908毫秒
  • 使用StringBuilder(换行符作为分开的附加)每次调用0.10201毫秒
  • 使用PrintWriter(StringWriter)每次调用0.10803毫秒

这么接近,对我来说时间几乎无关紧要,所以我将继续按照自己惯常的方式进行操作:使用StringBuilder(分开的附加),因为风格原因。当然,在这个已经成熟的代码库中工作时,我将大多遵循现有的约定,以尽量减少出人意料的情况。


结果并不令人意外。假定对 StringWriter 的任何特定调用 write(...) 只是在内部的 StringBuilder 上执行一次相应的 append(...) 调用,那么这正是 Java JIT 旨在优化掉的样板文件。 - jdmichal
1
@jdmichal StringWriter 使用 **StringBuffer**,它是 StringBuildersynchronized 替代品。因此 JIT 也需要删除不必要的锁定 - 相比以前,现在它做得更好了。尽管如此,这个问题并不简单,性能的相似之处可能比你想象的更令人惊讶... - Boris the Spider
相对而言,您的测试显示StringWriter比StringBuilder慢**6%(因为它在内部使用同步的StringBuffer并且有自己的开销),而错误的换行符附加比正确的StringBuilder用法慢17%**。虽然所花费的时间不是两倍或更多,但我不会说“无关紧要”,特别是如果您正在处理大量数据。另外,你好,这个帖子已经快10年了 :-) - Tobia

16

写入器 - PrintWriter 将数据写入流中,可以进行一些奇怪的操作,例如抑制IOExceptions。System.out就是其中之一。 - StringWriter 是一个将数据写入StringBuffer(类似于ByteArrayOutputStream用于OutputStreams)的Writer。

字符串构建器 - 当然,如果你只想在内存中构建字符串,StringBuilder就是你想要的。

结论

设计上,写入器旨在将字符数据推送到流中(通常是文件或通过连接),因此能够使用写入器来简单地组装字符串似乎是一个副作用。

StringBuilder(以及其同步的兄弟StringBuffer)被设计用于组装字符串(移动字符串)。如果查看StringBuilder API,你会发现不仅可以进行普通的追加操作,还可以进行替换、反转、插入等操作。StringBuilder是你构建字符串的好帮手。


谢谢,是的,我知道它们各自的作用。但是我在一些非常聪明的人编写的代码中遇到了这种新的习惯用法(对我来说是新的),我想知道他们是否有充分的理由喜欢它。 - CPerkins
好的,没问题。我已经在答案中添加了一个结论部分。 - krock
所以你的理由基本上是出于风格考虑:它依赖于 StringBuilder 的意图是操作字符串,而使用 Writers 来做到这一点只是一个副作用。 - CPerkins

7

我认为PrintWriter方法有两个优点: (1) 你不需要在每一行最后添加“+ newline”代码,如果要写很多内容,实际上会使代码更短。 (2) 你不需要调用System.getProperty()来让PrintWriter自行处理换行符应该是什么。


好的想法。我不太担心需要调用System.getProperty(),因为这是一次性的成本,如果像你说的那样,你正在编写大量代码,它会摊销为零。但是,使用StringBuilder时需要注意必须添加+ newline,这绝对是一个好观点。 - CPerkins

2

0

查看 JDK 11 中 StringWriter 的实现,它在底层使用了 StringBuffer。StringBuffer 是 StringBuilder 的同步版本,详情请参见 这篇关于 StringBuilder vs. StringBuffer 的问题

考虑到这一点,StringBuilder 可能会稍微快一些。因此,如果你编写对性能敏感的代码,请优先选择 StringBuilder,除非你需要同步。

在我的情况下,我必须在使用 StringBuilder 或 StringWriter 作为 Appendable 之间进行选择,而我不需要 StringWriter/StringBuffer 的同步功能。

总结一下:

  • StringBuilder - 非同步内存中追加
  • StringBuffer - 同步内存中追加
  • StringWriter - 包装 StringBuffer,还扩展了 Writer

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