Если вы хотите иметь 1 поток через список, вам нужен способ управления двумя различными состояниями, вы можете сделать это, внедрив Consumer в новый класс.

могу выполнить несколько несвязанных операций над элементами одного потока?

Скажи у меня есть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 содержал много элементов.

Можно ли как-то выполнить эти две операции, не проходя дважды по списку?

 Holger04 окт. 2017 г., 09:18
@ 123-xyz уже ударил гвоздь по голове. Как забавный факт, вы не заботитесь о повторении дважды, когда говоритеcontains("for")сразу послеindexOf("for"), несмотря на то, что эти операциинаходятся связанные и могут быть объединены без ущерба для читабельности. И если вы действительно заботитесь о производительности, подумайте о том, чтоt.split(" ") на самом деле делает, когда все, что вы хотите, это посчитать количество пробелов. Повторение дважды, когда операции не связаны, не является проблемой.
 MC Emperor04 окт. 2017 г., 10:36
Возможно, я должен был сформулировать проблему более точно. Я попытался привести пример вместе с постом, и поэтому мне пришлось придумать простой вариант использования. Этот вариант использования не имеет смысла вообще, и он также плохо реализован: действительно, применениеt.split(" ") чтобы получить только количество слов на самом делеявляется Плохо. Но что, если у меня нет списка с элементами, ноStream полученный из сетевого соединения или некоторого канала ввода / вывода - это значительно изменило бы вариант использования, не так ли?
 123-xyz03 окт. 2017 г., 19:57
ИМО, совершенно не нужно усложнять ваш код из-за несуществующей проблемы с производительностью. Перебор списка / коллекции в памяти довольно быстрый и быстрый, даже большой. Опять же, IMO, если я тот человек, который принимает решение при рассмотрении вашего кода, я приму код в OP и отклоню код в принятом ответе.
 Hulk04 окт. 2017 г., 11:09
@MCEmperor: да, это может что-то изменить - в зависимости от размера потока, собирающего его в коллекцию, он все равно может стоить затрат памяти, если вам нужно использовать его для нескольких несвязанных операций, или это может быть совершенно невозможно (например, если поток является бесконечным - конечно, все несвязанные операции должны были бы быть короткими замыканиями в этом случае, который, вероятно, был бы довольно сложным в любом случае).
 123-xyz04 окт. 2017 г., 19:51
@MCEmperor - это будет совершенно другой вопрос / сценарий, если элементы поступят от вызовов network / db. Прежде всего, мы не должны дублировать вызовы сети / базы данных, даже если размер небольшой или всего один. Во-вторых, вы все равно можете сохранить результат вызова network / db во временный список, а затем сделать то, что вы сделали в OP. В-третьих, если размер слишком велик для сохранения или вы не хотите по каким-либо причинам, вы можете использоватьPair/Triple в функции отображения:Function<? super T, Pair<R1, R2>> mapper = ... прежде чем начать думать о создании какого-то сложного коллектора.

Ответы на вопрос(4)

Решение Вопроса

Если вы хотите один проходStream тогда вы должны использовать обычайCollector (распараллеливание возможно).

class Splitter {
  public List<String> words = new ArrayList<>();
  public List<Integer> counts = new ArrayList<>();

  public void accept(String s) {
    if(s.contains("of")) {
      counts.add(s.split(" ").length);
    } else if(s.contains("for")) {
      words.add(s.substring(s.indexOf("for")));
    }
  }

  public Splitter merge(Splitter other) {
    words.addAll(other.words);
    counts.addAll(other.counts);
    return this;
  }
}
Splitter collect = strs.stream().collect(
  Collector.of(Splitter::new, Splitter::accept, Splitter::merge)
);
System.out.println(collect.counts);
System.out.println(collect.words);
 MC Emperor03 окт. 2017 г., 16:11
Это прекрасно работает!

вам нужен способ управления двумя различными состояниями, вы можете сделать это, внедрив 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);

о один раз:

 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());

давайте посмотрим, как быстро / медленно перебирать список / коллекцию. Вот результат теста на моей машине по тесту производительности ниже:

Когда: длина списка строк = 100, номер потока = 1, циклы = 1000, единица измерения = миллисекунды

ОП: 0,013

Принятый ответ: 0,020

По функции счетчика: 0,010

Когда: длина списка строк = 1000_000, номер потока = 1, циклы = 100, единица измерения = миллисекунды

ОП: 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();
}
 Holger04 окт. 2017 г., 09:29
Я полагаю, заменив.map(t -> t.split(" ").length) с, например,.map(t -> 1+(int)t.chars() .filter(c -> c==' ').count()), оказывает большее влияние на большие наборы данных, чем сохранение итерации, учитывая, что происходит за кулисами в каждом случае ...

Ваш ответ на вопрос