JAVA中将ArrayList<String[]>高效转换为多行字符串

5
ArrayList<String[]> writtenClasses = new ArrayList<String[]>();
// usually there is functional code here that populates
// ArrayList<String[]> writtenClasses with variably 3000
// String[] objects always of exactly 8 lines each

ArrayList<String> processedClasses = new ArrayList<String>();
for(String[] classLines: writtenClasses)
{
    for(String classLine: classLines)
    {
        processedClasses.add(classLine);
    }
}

String result = "";
for(String fileLine: processedClasses)
{
    result += fileLine + "\n";
}

这是我的代码。它可以正常工作并产生我想要的结果,但速度很慢。每写入一个ArrayList项需要大约10毫秒,当处理更大的任务时就有些慢了。我怀疑与ArrayList有关的某些问题正在导致速度缓慢,但在每次运行后对作业统计进行计时和控制台输出后却没有找到明显的问题。

上述代码是早期代码的改编,旨在提高效率。它比以前的方法提高了约4%。下面是我使用的旧方法,比上面稍微慢一点。

for(String[] classLines: writtenClasses)
{
    for(String classLine: classLines)
    {
        result += classLine + "\n";
    }
    writtenClasses.set(writtenClasses.indexOf(classLines), null);
}

我之所以使用 writtenClasses.set(writtenClasses.indexOf(classLines), null);,仅是出于内存效率的目的,并且我的统计数据显示它在使用更高效的内存时,CPU开销是无法检测到的。

这是我在StackOverflow上的第二个问题,我已尽力遵守规则,但如果我的提问有误或者在某些方面不小心冒犯了他人,请指出来,我会加以改正。 :)


尝试使用 StringBuilder 而不是字符串拼接,看看是否有助于提高性能? - Michael0x2a
只是出于兴趣 - 你能在没有使用StringBuilder的情况下运行你的解决方案吗?(见下面https://dev59.com/vIPba4cB1Zd3GeqPpDe_#25737459的评论)并查看是否会有明显的区别?似乎大多数人(包括我自己)都不知道这是“上个十年的编码方法”... - Jan Groth
回复Jan:是的,我可以,但是非常非常慢。 - Scruffy
@Java-Now-A-Pro: 真的吗?这与评论所暗示的完全相反... :o - Jan Groth
回复Jan:对不起,我不明白...你所指的“评论”是哪一条? - Scruffy
5个回答

3

中间创建processedClasses列表是完全没有用的。此外,StringBuilder会显著提高处理速度:

// Consider a large initial size to even avoid reallocation, here I used 64 KB
StringBuilder sb = new StringBuilder(65536);

for (String[] classLines : writtenClasses)
    for (String lines : classLines)
        sb.append(lines).append('\n');

// Note: you might not even need to convert it to String, read reasoning below
String result = sb.toString();

我们使用实现了CharSequence接口的StringBuilder来构建内容。许多类接受CharSequence而不仅仅是String。一个很好的例子是FileWriter。在这些情况下,您甚至不需要将StringBuilder转换为String,因为如果内容非常大,StringBuilder可以像其String结果一样轻松地传递,这可能是另一个性能优势。

谢谢!在一个大型任务中,我的程序需要一个小时的时间,而你把它缩短到了一秒半。我同意你的第一个说法;将其分解是很痛苦的,因为这样做感觉不对。我意识到我不应该在内存中拥有千兆字节长的字符串,但我必须问一下,当一个StringBuilder被填满了一个2^31长的字符串并且被附加时会发生什么。也许需要单独询问这个问题... 干杯! - Scruffy
它会崩溃并抛出一个 OutOfMemoryException: Requested array size exceeds VM limit,就像几乎所有基于数组和指数增长的数据结构一样。 “2^31个条目对于任何人来说应该足够了” :( - Clément MATHIEU

3

其他答案已经指出了问题。使用Java 8,替代两个嵌套循环和StringBuilder的方法是使用流和joining收集器*:

String result = writtenClasses.stream()
        .flatMap(array -> Arrays.stream(array))
        .collect(joining("\n"));

*需要导入静态的java.util.Collectors.joining;


1

虽然这不是一个恰当的回答,但在评论中阅读起来太尴尬了:

String result = "";
for(String fileLine: processedClasses)
{
    result += fileLine + "\n";
}

那会创建一百万个字符串实例。我猜在这里使用 StringBuilder 应该对性能有积极影响。

你的示例并不会创建数百万个字符串实例。这种非常常见的用例自至少十年以来已经被所有供应商进行了优化。Javac检测到这种模式并自动将字符串连接替换为StringBuilder。您可以使用javap轻松查看它。然而,OP代码中的双重循环会欺骗编译器,并为每个外部循环创建一个字符串。简而言之,在理论上你是正确的,在实践中,像你的示例这样的简单和规则的循环并不重要。 - Clément MATHIEU

1
这里的主要痛点可能不是 ArrayList,而是在使用字符串 + 运算符时。由于 Java 中的字符串是不可变的,每次调用都会强制创建一个新对象并复制所有数据,正如您所述,这可能需要很长时间。
更快的方法是使用 StringBuilder,它不会(必须)在每个操作中强制复制数据:
StringBuilder result = new StringBuilder();
for(String[] classLines: writtenClasses)
{
    for(String classLine: classLines)
    {
        result.append(classLine).append('\n');
    }
}

0

基于这个问题

ewall:

At what point do you switch to StringBuilder? When it effects memory or performance. Or when it might. If you're really only doing this for a couple strings once, no worries. But if you're going to be doing it over and over again, you should see a measurable difference when using StringBuilder.

StringBuilder myString = new StringBuilder();

     for(String classLine: classLines)
        {
           myString.append(classLine).append("\n");
        }

StringBuilder 可以在某种程度上提高你的性能。


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