将Java StringBuilder转储到文件

46
什么是将 StringBuilder 导出到文本文件的最有效/优雅方式?
你可以这样做:
outputStream.write(stringBuilder.toString().getBytes());

但对于非常长的文件来说,这是否高效?

有更好的方法吗?


“非常长”你会觉得有多大?大小是以KB,MB还是更多为单位呢?StringBuilder的大小是否会接近任何实际限制,例如分配给JVM的最大内存? - rob
可能几兆字节... - Patrick
我离不开StringBuilder,因为它是我的API返回的内容。 - Patrick
9个回答

46
正如其他人所指出的,应该使用Writer,并且使用BufferedWriter,但是不要调用writer.write(stringBuilder.toString());,而是只需调用writer.append(stringBuilder);
编辑:但是,我看到您接受了另一个答案,因为它只有一行代码。但是这个解决方案有两个问题:
  1. 它不接受java.nio.Charset。不好。您应该始终明确指定Charset。

  2. 它仍然让您遭受stringBuilder.toString()。如果您真的想要简单性,请尝试来自Guava项目的以下内容:

Files.write(stringBuilder, file, Charsets.UTF_8)


1
writer.write(); 不接受 StringBuilder 作为参数。我可以使用 FileUtils.writeStringToFile(file, String, String encoding) 指定编码。 - Patrick
非常抱歉 - 应该是writer.append()!正在修复。 - Kevin Bourrillion
3
谢谢你关于FileUtils的说明,但是用字符串作为字符集的指定方式有些低效。 - Kevin Bourrillion
10
查看Writer.append()的源代码,我们可以看到它调用了write(csq.toString()),因此仍然在字符串生成器对象上调用了toString()方法。因此没有获得任何收益。 - Graham Seed
我给它点了踩,因为它没有展示完整的例子。 - Brett Sutton
显示剩余2条评论

31

您应该使用BufferedWriter来优化写入操作(始终使用Writer而不是OutputStream来写入字符数据)。如果您不是在写字符数据,则应使用BufferedOutputStream。

File file = new File("path/to/file.txt");
BufferedWriter writer = null;
try {
    writer = new BufferedWriter(new FileWriter(file));
    writer.append(stringBuilder);
} finally {
    if (writer != null) writer.close();
}

或者使用try-with-resources(Java 7及以上版本)

File file = new File("path/to/file.txt");
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
    writer.append(stringBuilder);
}

由于你最终要写入文件,一个更好的方法是在处理过程中更频繁地向BufferedWriter中写入,而不是在内存中创建一个巨大的StringBuilder并在最后一次性写入所有内容(根据你的使用情况,甚至可以完全消除 StringBuilder)。在处理期间逐步写入将节省内存,并更好地利用有限的I/O带宽,除非另一个线程正在尝试从磁盘读取大量数据时你正在写入。


好的回答,谢谢。但为了完整起见:您还可以调用writer.flush()和/或writer.close()以确保在程序或线程终止之前实际写入完整的字符串。 - sebers
第一个版本在Java 5中无法编译。在finally子句中找不到Writer。 - narthur157
@sebers 感谢您指出我们需要有'writer.close();'. 我曾经面临过这样一个问题,即我的所有字符串都没有被写入文件中,我调试了好几个小时,但无法找到原因。然后我尝试了'writer.close();', 一切都完美地解决了。 - FullStackDeveloper

20
你可以使用Apache Commons IO库,它提供了FileUtils:

你可以使用Apache Commons IO库,它给你FileUtils

FileUtils.writeStringToFile(file, stringBuilder.toString(), Charset.forName("UTF-8"))

我选择这个作为首选答案,因为它将复杂性抽象化了。虽然它可能不是最有效的,但其他答案也很好,如果效率开始受到影响,我可能会使用它们。 - Patrick
这个问题有两个主要的问题,我在我的答案中已经解释了。 - Kevin Bourrillion

16

如果字符串很大,toString().getBytes()将会创建重复的字节(2或3次),这取决于字符串的大小。

为了避免这种情况,你可以将字符串分块并分别写入。

以下是可能的实现方式:

final StringBuilder aSB = ...;
final int    aLength = aSB.length();
final int    aChunk  = 1024;
final char[] aChars  = new char[aChunk];

for(int aPosStart = 0; aPosStart < aLength; aPosStart += aChunk) {
    final int aPosEnd = Math.min(aPosStart + aChunk, aLength);
    aSB.getChars(aPosStart, aPosEnd, aChars, 0);                 <i>// Create no new buffer</i>
    final CharArrayReader aCARead = new CharArrayReader(aChars); <i>// Create no new buffer</i>

    <i>// This may be slow but it will not create any more buffer (for bytes)</i>
    int aByte;
    while((aByte = aCARead.read()) != -1)
        outputStream.write(aByte);
}

希望这能帮到你。


3
并不是真的变慢了,我测试过一个50MB的字符串,只是它真的节省内存(相较于其他方法,约为2MB对比130MB)。 - mhaller
@NawaMan “大”的性能差异来自底层的OutputStream。在许多情况下,write(array)方法调用在内部分解为while循环。不错的例子。 - user166390
这比 .append 解决方案更节省内存吗?我想写入器可能在幕后做着类似的事情。 - Thomas Ahle
据我所知(并尝试过),如果不是最有效的,那么append就是其中之一。另一个对于Stream非常高效的方法是write(byte)。Java现在是开源的,所以你可以看到代码,我记得append和write的实现总是相关的。 - NawaMan
@NawaMan 是的,我刚刚检查了一下,append(CharacterStream cs) = write(cs.toString())。 - Thomas Ahle

4

对于字符数据,最好使用 Reader/Writer,在您的情况下,请使用 BufferedWriter。如果可能,一开始就使用 BufferedWriter 而不是 StringBuilder 以节省内存。

请注意,您调用非参数 getBytes() 方法的方式将使用平台默认字符编码来解码字符。如果平台默认编码为例如 ISO-8859-1 而您的字符串数据包含 ISO-8859-1 字符集之外的字符,则可能失败。最好使用 getBytes(charset),其中您可以自己指定字符集,例如 UTF-8


4
自Java 8以来,您只需要执行以下操作即可:
Files.write(Paths.get("/path/to/file/file_name.extension"), stringBuilder.toString().getBytes());
您无需任何第三方库来完成此操作。

问题在于如果相应的字符串很大,则调用stringBuilder.toString()。而您的答案并没有帮助到这个问题。 - Eric Duminil

1

这里提供大多数答案的基准测试和改进实现: https://www.genuitec.com/dump-a-stringbuilder-to-file/

最终的实现方式如下:

try {
    BufferedWriter bw = new BufferedWriter(
            new OutputStreamWriter(
                    new FileOutputStream(file, append), charset), BUFFER_SIZE);
    try {
        final int length = sb.length();
        final char[] chars = new char[BUFFER_SIZE];
        int idxEnd;
        for ( int idxStart=0; idxStart<length; idxStart=idxEnd ) {
            idxEnd = Math.min(idxStart + BUFFER_SIZE, length);
            sb.getChars(idxStart, idxEnd, chars, 0);
            bw.write(chars, 0, idxEnd - idxStart);
        }
        bw.flush();
    } finally {
        bw.close();
    }
} catch ( IOException ex ) {
    ex.printStackTrace();
}

1
“idxStart=idxEnd” 是一个特性还是一个错误? - Eric Duminil
绝对是一个特性... - dotwin

1
基于https://dev59.com/enI-5IYBdhLWcg3wxrqQ#1677317,我创建了这个函数,使用OutputStreamWriterwrite(),这也是内存优化的方式,比仅使用StringBuilder.toString()更好。
public static void stringBuilderToOutputStream(
        StringBuilder sb, OutputStream out, String charsetName, int buffer)
        throws IOException {
    char[] chars = new char[buffer];
    try (OutputStreamWriter writer = new OutputStreamWriter(out, charsetName)) {
        for (int aPosStart = 0; aPosStart < sb.length(); aPosStart += buffer) {
            buffer = Math.min(buffer, sb.length() - aPosStart);
            sb.getChars(aPosStart, aPosStart + buffer, chars, 0);
            writer.write(chars, 0, buffer);
        }
    }
}

1

如果字符串本身很长,那么一定要避免使用toString(),因为它会制作另一个字符串副本。最高效的写入流方法应该像这样:

OutputStreamWriter writer = new OutputStreamWriter(
        new BufferedOutputStream(outputStream), "utf-8");

for (int i = 0; i < sb.length(); i++) {
    writer.write(sb.charAt(i));
}

2
请不要使用writer.append(sb)。它与writer.write(sb.toString())相同,因此失去了意义。 - ZZ Coder
逐个字符地编写它并不是很高效,基于缓冲区的解决方案可能更快。 - Eric Duminil

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