在Java

时间:2017-10-03 07:58:58

标签: java java-stream

如何对单个流的元素执行多个不相关的操作?

说我有一个由文本组成的List<String>。列表中的每个字符串可能包含也可能不包含某个单词,表示要执行的操作。让我们说:

  • 如果字符串包含'of',则该字符串中的所有单词都必须计算
  • 如果字符串包含'for',则必须返回第一次出现'for'后的部分,产生带有所有子字符串的List<String>

当然,我可以这样做:

List<String> strs = ...;

List<Integer> wordsInStr = strs.stream()
    .filter(t -> t.contains("of"))
    .map(t -> t.split(" ").length)
    .collect(Collectors.toList());

List<String> linePortionAfterFor = strs.stream()
    .filter(t -> t.contains("for"))
    .map(t -> t.substring(t.indexOf("for")))
    .collect(Collectors.toList());

但随后列表将被遍历两次,如果strs包含大量元素,则可能导致性能下降。

是否有可能以某种方式执行这两个操作而不在列表上遍历两次?

4 个答案:

答案 0 :(得分:5)

如果您想要一次通过.keys(),则必须使用自定义Stream(可能并行化)。

Collector

答案 1 :(得分:4)

以下是从不同方面解决OP的答案。首先,让我们来看看迭代列表/集合的速度有多快/慢。以下是我的机器上的测试结果:

时间:字符串列表长度= 100,线程数= 1,循环= 1000,单位=毫秒

OP:0.013

接受的答案:0.020

通过计数器功能:0.010

时间:字符串列表长度= 1000_000,线程数= 1,循环= 100,单位=毫秒

OP:99.387

接受的答案:89.848

通过计数器功能:59.183

结论:性能提升的百分比非常小甚至更慢(如果字符串列表的长度很小)。通常,减少由更复杂的收集器加载到内存中的列表/集合的迭代是错误的。你没有获得太多的性能提升。如果存在性能问题,我们应该调查其他地方。

以下是我使用工具Profiler的性能测试代码:(我不打算在此讨论如何进行性能测试。如果您怀疑测试结果,可以使用任何工具再次执行此操作你相信)

@Test
public void test_46539786() {
    final int strsLength = 1000_000;
    final int threadNum = 1;
    final int loops = 100;
    final int rounds = 3;

    final List<String> strs = IntStream.range(0, strsLength).mapToObj(i -> i % 2 == 0 ? i + " of " + i : i + " for " + i).toList();

    Profiler.run(threadNum, loops, rounds, "OP", () -> {
        List<Integer> wordsInStr = strs.stream().filter(t -> t.contains("of")).map(t -> t.split(" ").length).collect(Collectors.toList());
        List<String> linePortionAfterFor = strs.stream().filter(t -> t.contains("for")).map(t -> t.substring(t.indexOf("for")))
                .collect(Collectors.toList());

        assertTrue(wordsInStr.size() == linePortionAfterFor.size());
    }).printResult();

    Profiler.run(threadNum, loops, rounds, "Accepted answer", () -> {
        Splitter collect = strs.stream().collect(Collector.of(Splitter::new, Splitter::accept, Splitter::merge));
        assertTrue(collect.counts.size() == collect.words.size());
    }).printResult();

    final Function<String, Integer> counter = s -> {
        int count = 0;
        for (int i = 0, len = s.length(); i < len; i++) {
            if (s.charAt(i) == ' ') {
                count++;
            }
        }
        return count;
    };

    Profiler.run(threadNum, loops, rounds, "By the counter function", () -> {
        List<Integer> wordsInStr = strs.stream().filter(t -> t.contains("of")).map(counter).collect(Collectors.toList());
        List<String> linePortionAfterFor = strs.stream().filter(t -> t.contains("for")).map(t -> t.substring(t.indexOf("for")))
                .collect(Collectors.toList());

        assertTrue(wordsInStr.size() == linePortionAfterFor.size());
    }).printResult();
}

答案 2 :(得分:1)

您可以使用自定义收集器并仅迭代一次:

 private static <T, R> Collector<String, ?, Pair<List<String>, List<Long>>> multiple() {

    class Acc {

        List<String> strings = new ArrayList<>();

        List<Long> longs = new ArrayList<>();

        void add(String elem) {
            if (elem.contains("of")) {
                long howMany = Arrays.stream(elem.split(" ")).count();
                longs.add(howMany);
            }
            if (elem.contains("for")) {
                String result = elem.substring(elem.indexOf("for"));
                strings.add(result);
            }

        }

        Acc merge(Acc right) {
            longs.addAll(right.longs);
            strings.addAll(right.strings);
            return this;
        }

        public Pair<List<String>, List<Long>> finisher() {
            return Pair.of(strings, longs);
        }

    }
    return Collector.of(Acc::new, Acc::add, Acc::merge, Acc::finisher);
}

用法是:

Pair<List<String>, List<Long>> pair = Stream.of("t of r m", "t of r m", "nice for nice nice again")
            .collect(multiple());

答案 3 :(得分:0)

如果您希望通过列表拥有1个流,则需要一种方法来管理2个不同的状态,您可以通过将Consumer实现为新类来实现。

    class WordsInStr implements Consumer<String> {

      ArrayList<Integer> list = new ArrayList<>();

      @Override
      public void accept(String s) {
        Stream.of(s).filter(t -> t.contains("of")) //probably would be faster without stream here
            .map(t -> t.split(" ").length)
            .forEach(list::add);
      }
    }

    class LinePortionAfterFor implements Consumer<String> {

      ArrayList<String> list = new ArrayList<>();

      @Override
      public void accept(String s) {
        Stream.of(s) //probably would be faster without stream here
            .filter(t -> t.contains("for"))
            .map(t -> t.substring(t.indexOf("for")))
            .forEach(list::add);
      }
    }

    WordsInStr w = new WordsInStr();
    LinePortionAfterFor l = new LinePortionAfterFor();

    strs.stream()//stream not needed here
        .forEach(w.andThen(l));
    System.out.println(w.list);
    System.out.println(l.list);