【问题标题】:How to improve pervormance of String processing using Stream#reduce?如何使用 Stream#reduce 提高字符串处理的性能?
【发布时间】:2021-10-21 06:52:33
【问题描述】:

在我的旧项目中,此代码对 10000 个元素执行超过一分钟

private ByteArrayInputStream getInputStreamFromContactFile(MyDTO contacts) {
        long start = System.currentTimeMillis();
        try {
            byte[] bytes = contacts.getLines()
                    .stream()
                    .map(lineItem -> lineItem.value)
                    .reduce(contacts.getHeader().concat("\n"), (partialString, el) -> partialString + el+ '\n')
                    .getBytes();
            return new ByteArrayInputStream(bytes);
        } finally {
            log.info("Duration is {}ms", System.currentTimeMillis() - start);
        }

有什么明显的方法可以让它更快吗?

【问题讨论】:

  • 你基本上只是想加入\n的行?
  • 这能回答你的问题吗? Java: convert List<String> to a String
  • @WJS 正如我已经提到的,这段代码是遗留代码
  • 并不是指you 个人。但我想你知道每个映射/方法返回什么以及预期的最终结果。
  • 返回类型是ByteArrayInputStream 还是InputStream 就足够了?另一点:代码在语义上是正确的。归约函数不是关联的,并且单位元并不是真正的单位元。 Java Streamreduce 不是左折叠。

标签: java string performance java-stream


【解决方案1】:

为了提高性能,最好使用中间ByteArrayOutputStream + OutputStreamWriter 来连接值。

拼接结果的字节数组由ByteArrayOutputStream::toByteArray返回

private ByteArrayInputStream getInputStreamFromContactFile(MyDTO contacts) throws IOException {
    long start = System.currentTimeMillis();
    try {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Writer writer = new OutputStreamWriter(bos);
        writer.write(contacts.getHeader());
        writer.write("\n");

        contacts.getLines().forEach(line -> { 
            try {
                writer.write(line.value);
                writer.write("\n");
            } catch (IOException ioex) { throw new RuntimeException(ioex);}
        });
        writer.flush();

        return new ByteArrayInputStream(bos.toByteArray());
    } finally {
        log.info("Duration is {}ms", System.currentTimeMillis() - start);
    }
}

另一种方法是使用Collectors.joining with prefix and suffix

private ByteArrayInputStream getInputStreamFromContactFile(MyDTO contacts) {
    long start = System.currentTimeMillis();
    try {
        return new ByteArrayInputStream(
            contacts.getLines()
                .stream()
                .map(item -> item.value)
                .collect(Collectors.joining("\n", contacts.getHeader().concat("\n"), "\n"))
                .getBytes()
        );
    } finally {
        log.info("Duration is {}ms", System.currentTimeMillis() - start);
    }
}

如果确实需要对StringBuilder使用Stream::reduce操作(由于某种原因),可以应用following approach

private static ByteArrayInputStream getInputStreamFromContactFileReducing(MyDTO contacts) {

    long start = System.currentTimeMillis();
    try {
        byte[] bytes = contacts.getLines()
                               .stream()
                               .map(lineItem -> lineItem.value)
                               .reduce(new StringBuilder().append(contacts.getHeader()).append("\n"),
                                       (sb, line) -> sb.append(line).append('\n'),
                                       (sb1, sb2) -> sb1.append(sb2))
                               .toString()
                               .getBytes();
        return new ByteArrayInputStream(bytes);
    } finally {
        log.info("Reducing: Duration is {}ms", System.currentTimeMillis() - start);
    }
}

【讨论】:

  • 为什么你的第二种方法应该比我的主题中的原始方法更好?你能解释一下吗?
  • 这种形式的Collectors.joining 使用基于StringJoinerStringBuilder,这里应该创建StringBuilder 的一个实例——它基本上类似于@LeonardBrünings 的答案。在原始代码中,reduce 部分:partialString + el+ '\n' 中的连接很可能会创建多个 Strings/StringBuilders。
【解决方案2】:

如果速度真的很重要,使用 StringBuilder 会有所帮助,但看起来功能性较差。

StringBuilder builder = new StringBuilder();
builder.append(contacts.getHeader());
builder.append("\n");
contacts.getLines()
    .stream()
    .map(lineItem -> lineItem.value)
    .forEach(line -> {
      builder.append(line);
      builder.append("\n");
    });
builder.toString().getBytes();

【讨论】:

  • 这看起来有点奇怪....builder.append(contacts.getLines() 和你的内心 builder.append...
  • 是的,这是错误的,我在编辑答案时搞砸了。
  • list.stream().map().forEach()有点多余,可以改成list.forEach(line -&gt; builder.append(line.value).append("\n"))
  • 谢谢!现在平均需要 20-40 毫秒。 ot 之前是 50 秒。我真的很惊讶。
  • 嗯,@WJS 的答案似乎是最快的,因为它使用普通循环,没有 Stream API 的开销。
【解决方案3】:

好吧,对于超过10_000_000 行,每行36 个字符,这在4 秒内运行。但不确定它是否符合您的要求。

private ByteArrayInputStream getInputStreamFromContactFile(MyDTO contacts) {
    long start = System.currentTimeMillis();
    try {
       StringBuilder sb = new StringBuilder(contacts.getHeader()).append("\n");
       for (String lineItem : contacts.getLines()) {
          sb.append(lineItem).append("\n");
        }
        return new ByteArrayInputStream(sb.toString().getBytes());

     } finally {
        log.info("Duration is {}ms", System.currentTimeMillis() - start);
     }
}

【讨论】:

  • 将在我的环境准备好后立即分享我的结果
  • 是的,它奏效了。我在这里分享了我的结果stackoverflow.com/questions/68849950/…
  • 请注意,getBytes() 使用系统的默认字符集。最好明确指定一个字符集。您可以使用ByteBuffer bb = Charset.defaultCharset().encode(CharBuffer.wrap(sb)); return new ByteArrayInputStream(bb.array(), bb.arrayOffset(), bb.limit());,它执行相同的操作,但直接对StringBuilder 的内容进行编码,而无需先将其复制到新的String 中。
  • @Holger 当然,这很明显。 :) 我将不得不查看源代码并确切了解发生了什么。这是我从未想过要做的事情。
猜你喜欢
  • 1970-01-01
  • 2019-09-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-07-11
  • 1970-01-01
相关资源
最近更新 更多