使用非常大的字符串是否不好?(Java)

16

创建大字符串有什么负面影响吗?例如,如果我们从一个可能非常巨大的文本文件中读取文本:

while (scanner.hasNext()) {
  someString += scanner.next();
}
// do something cool with some string

逐行处理文件是否(通常)是更好的解决方案,为什么?

6个回答

53

流式处理 vs 非流式处理

如果您使用流式处理,您可以处理任何大小的文件(假设您确实可以忘记已经查看的所有数据)。 您最终会得到一个自然的O(n)复杂度,这是非常好的。您不会因为内存不足而中断处理。

流式处理很棒... 但并不适用于每种情况。

StringBuilder

由于StringBuilder建议存在一定争议,因此在此提供基准测试来展示其效果。为了让慢版本能够在合理的时间内完成,我不得不减少基准测试的大小。

首先是结果,然后是代码。 这是一个非常粗略的基准测试,但结果足以证明这一点...

c:\Users\Jon\Test>java Test slow
Building a string of length 120000 without StringBuilder took 21763ms

c:\Users\Jon\Test>java Test fast
Building a string of length 120000 with StringBuilder took 7ms

而且这段代码...

class FakeScanner
{
    private int linesLeft;
    private final String line;

    public FakeScanner(String line, int count)
    {
        linesLeft = count;
        this.line = line;
    }

    public boolean hasNext()
    {
        return linesLeft > 0;
    }

    public String next()
    {
        linesLeft--;
        return line;
    }
}

public class Test
{    
    public static void main(String[] args)
    {
        FakeScanner scanner = new FakeScanner("test", 30000);

        boolean useStringBuilder = "fast".equals(args[0]);

        // Accurate enough for this test
        long start = System.currentTimeMillis();

        String someString;
        if (useStringBuilder)
        {
            StringBuilder builder = new StringBuilder();
            while (scanner.hasNext())
            {
                builder.append(scanner.next());
            }
            someString = builder.toString();
        }
        else
        {
            someString = "";     
            while (scanner.hasNext())
            {
                someString += scanner.next();
            }        
        }
        long end = System.currentTimeMillis();

        System.out.println("Building a string of length " 
                           + someString.length()
                           + (useStringBuilder ? " with" : " without")
                           + " StringBuilder took " + (end - start) + "ms");
    }
}

1
编译这个!它会变成 StringBuilder! - Stefan Kendall
2
@iftrue:它将把它转换为使用StringBuilder,然后调用toString。 - Jon Skeet
但这是初学者的问题,他回答得很可悲。 - IAdapter
@01: 我不明白你的评论。能否详细说明一下? - Jon Skeet
Jon,做得好。 数字总是最好的答案。顺便说一句,我重新运行了你的测试,并重构了运行这两个测试的多个时间(以避免任何可能的类加载延迟)。你肯定不会感到惊讶,听到我得到了与你非常相似的结果。 - CPerkins

4

我相信每次使用+=运算符会创建一个新的字符串对象。建议使用StringBuilder代替。


1
错误。编译代码并查看字节码。请查看我的回复。 - Stefan Kendall
2
取决于编译器。大多数编译器只会在同一行中使用 StringBuilder 进行替换(例如,“123”+“456”+“789”)。 - Keith Adler
不是我所见过的。我曾经有一篇文章,但我把它弄丢了。 - Stefan Kendall
3
怎么样我们来做一个基准测试?你似乎完全相信StringBuilder是没有用的,尽管编译后的代码在每次迭代中都创建了一个新的StringBuilder并调用了toString。该进行一次测试了… - Jon Skeet
对编译器优化的评论很好。然而,我仍然认为这个答案是有效的 - 在我看来,最好不要不必要地依赖于特定于实现的编译器优化。 - hrnt
显示剩余3条评论

1
使用 StringBuilder。你的方法可能会创建成千上万个一次性对象。字符串是不可变对象,这意味着一旦你创建了一个字符串,你就不能改变它...你只能创建一个新的字符串并将引用分配给当前实例。StringBuilder 在速度和内存方面将比字符串高出数百甚至数千倍。

http://java.sun.com/j2se/1.5.0/docs/api/java/lang/StringBuilder.html

大多数Java编译器现在会为您优化代码,但最好的做法是一开始就编写正确的代码。


2
StringBuilder更好,因为它没有同步。 - Jon Skeet
错误的。编译代码并查看字节码。根据情况,您会得到缓冲区或构建器。 - Stefan Kendall
1
@iftrue:错误。请查看对您答案的回复。 - T.J. Crowder
我认为(Java -> bytecode)编译器优化对所提供的代码不会有太大作用。 - Tom Hawtin - tackline

0
正如Jon Skeet所说,流式处理数据是一种更为健壮的方式。此外,字符串的大小有一个最大值Max_INT字符 - 因此,如果您的文件可能比这个更大,您应该尽可能考虑使用数据流处理。

0
如果输入的数据比系统内存大(例如,输入是通过HTTP连接从另一台计算机生成的),该怎么办?如果您逐行处理,您总是在取得进展,并且最终将处理整个输入,假设输入是有限的。但是,如果您等待看到整个输入,然后再执行任何处理,您将耗尽内存并崩溃。
通常,以流式方式处理数据是很好的选择。这也适用于使用迭代器而不是随机访问进行处理时。它将使您的程序能够扩展到非常大的输入大小,并且还允许您的程序进行管道化(即,另一个程序可以开始处理您的程序的输出,而您的程序仍在处理自己的输入)。在当今许多不同计算机之间进行大型媒体传输的时代,这几乎总是一个支持的好主意。

0

还有一些额外的要点:

  1. 如果你将大量数据读入 StringBuilder,然后调用 toString(),JVM 在转换期间会暂时需要双倍的 char[] 存储空间。如果你可以将数据处理为 CharSequenceStringBuilder 实现了 CharSequence),那么就可以避免这种情况。
  2. 如果你确实需要将所有数据读入内存,则可以将 String 表示为单词列表(即 List<String>),并在每个单词上调用 intern()。如果数据包含大量重复的单词,则这将表示显著的内存节省

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