Java 8-лучший способ преобразования списка: карта или foreach?


у меня есть список myListToParse где я хочу отфильтровать элементы и применить метод к каждому элементу, и добавить результат в другой список myFinalList.

С Java 8, я заметил, что я могу сделать это 2 способами. Хотелось бы узнать более эффективный способ между ними и понять, почему один способ лучше, чем другой.

Я открыт для любого предложения о третьем способе.

Способ 1:

myFinalList = new ArrayList<>();
myListToParse.stream()
        .filter(elt -> elt != null)
        .forEach(elt -> myFinalList.add(doSomething(elt)));

Способ 2:

myFinalList = myListToParse.stream()
        .filter(elt -> elt != null)
        .map(elt -> doSomething(elt))
        .collect(Collectors.toList()); 
8 135

8 ответов:

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

Метод 2 предпочтительнее, потому что

  1. это не требует мутации коллекции, которая существует вне лямбда-выражения,

  2. это более читабельно, потому что различные шаги, выполняемые в конвейере сбора, записываются последовательно (сначала операция фильтра, затем операция карты, а затем сбор результат), (для получения дополнительной информации о преимуществах коллекторных трубопроводов см. Martin Fowler отличная статья)

  3. вы можете легко изменить способ сбора значений, заменив Collector что используется. В некоторых случаях вам может понадобиться написать свой собственный Collector, но тогда преимущество в том, что вы можете легко использовать это.

Я согласен с существующими ответами, что вторая форма лучше, потому что она не имеет никаких побочных эффектов и легче распараллеливается (просто используйте параллельный поток).

С точки зрения производительности, похоже, что они эквивалентны, пока вы не начнете использовать параллельные потоки. В таком случае, карта будет работать действительно намного лучше. Смотрите ниже микро-тест результаты:

Benchmark                         Mode  Samples    Score   Error  Units
SO28319064.forEach                avgt      100  187.310 ± 1.768  ms/op
SO28319064.map                    avgt      100  189.180 ± 1.692  ms/op
SO28319064.mapWithParallelStream  avgt      100   55,577 ± 0,782  ms/op

вы не можете увеличить первый пример таким же образом, потому что forEach является терминальным методом-он возвращает void-поэтому вы вынуждены использовать лямбда с сохранением состояния. Но это очень плохая идея, если вы используете параллельные потоки.

наконец, обратите внимание, что ваш второй фрагмент может быть написан в более сжатом виде с помощью ссылок на методы и статического импорта:

myFinalList = myListToParse.stream()
    .filter(Objects::nonNull)
    .map(this::doSomething)
    .collect(toList()); 

предполагая, что причина, по которой вы изучаете этот стиль программирования, заключается в том, что вы хотите использовать эти преимущества, тогда ваш первый пример кода потенциально не работает так как foreach метод классифицируется как терминальный (это означает, что он может вызывать побочные эффекты).

второй способ предпочтителен с точки зрения функционального программирования, так как функция map может принимать лямбда-функции без состояния. Более явно, лямбда, переданная функции map, должна быть

  1. не вмешивается, что означает, что функция не должна изменять источник потока, если он не является параллельным (например,ArrayList).
  2. без избегайте неожиданных результатов при параллельной обработке (вызванной различиями в расписании потоков).

еще одно преимущество второго подхода заключается в том, что если поток параллелен, а коллектор параллелен и неупорядочен, то эти характеристики могут предоставить полезные подсказки для операции сокращения для одновременного сбора.

Если вы используете Коллекции Eclipse можно использовать collectIf() метод.

MutableList<Integer> source =
    Lists.mutable.with(1, null, 2, null, 3, null, 4, null, 5);

MutableList<String> result = source.collectIf(Objects::nonNull, String::valueOf);

Assert.assertEquals(Lists.immutable.with("1", "2", "3", "4", "5"), result);

он оценивает охотно и должен быть немного быстрее, чем с помощью потока.

Примечание: я коммиттер для коллекций Eclipse.

Я предпочитаю второй способ.

при использовании первого способа, Если вы решите использовать параллельный поток для повышения производительности, вы не будете иметь никакого контроля над порядком, в котором элементы будут добавлены в список вывода по forEach.

при использовании toList, API потоков сохранит порядок, даже если вы используете параллельный поток.

есть и третий вариант-с помощью stream().toArray() - см. комментарии почему у stream не было метода toList. Он оказывается медленнее, чем forEach() или collect (), и менее выразителен. Он может быть оптимизирован в более поздних сборках JDK, поэтому добавьте его здесь на всякий случай.

предполагая, что List<String>

    myFinalList = Arrays.asList(
            myListToParse.stream()
                    .filter(Objects::nonNull)
                    .map(this::doSomething)
                    .toArray(String[]::new)
    );

С микро-микро бенчмарк, 1м записей, 20% нулей и простое преобразование в doSomething ()

private LongSummaryStatistics benchmark(final String testName, final Runnable methodToTest, int samples) {
    long[] timing = new long[samples];
    for (int i = 0; i < samples; i++) {
        long start = System.currentTimeMillis();
        methodToTest.run();
        timing[i] = System.currentTimeMillis() - start;
    }
    final LongSummaryStatistics stats = Arrays.stream(timing).summaryStatistics();
    System.out.println(testName + ": " + stats);
    return stats;
}

результаты являются

параллель:

toArray: LongSummaryStatistics{count=10, sum=3721, min=321, average=372,100000, max=535}
forEach: LongSummaryStatistics{count=10, sum=3502, min=249, average=350,200000, max=389}
collect: LongSummaryStatistics{count=10, sum=3325, min=265, average=332,500000, max=368}

последовательный:

toArray: LongSummaryStatistics{count=10, sum=5493, min=517, average=549,300000, max=569}
forEach: LongSummaryStatistics{count=10, sum=5316, min=427, average=531,600000, max=571}
collect: LongSummaryStatistics{count=10, sum=5380, min=444, average=538,000000, max=557}

параллельный без нулей и фильтра (так что поток SIZED): toArrays имеет лучшую производительность в таком случае, и .forEach() не удается с "indexOutOfBounds" на приемном ArrayList, пришлось заменить на .forEachOrdered()

toArray: LongSummaryStatistics{count=100, sum=75566, min=707, average=755,660000, max=1107}
forEach: LongSummaryStatistics{count=100, sum=115802, min=992, average=1158,020000, max=1254}
collect: LongSummaryStatistics{count=100, sum=88415, min=732, average=884,150000, max=1014}

может быть Метод 3.

Я всегда предпочитаю держать логику отдельно.

Predicate<Long> greaterThan100 = new Predicate<Long>() {
            @Override
            public boolean test(Long currentParameter) {
                return currentParameter > 100;
            }
        };

        List<Long> sourceLongList = Arrays.asList(1L, 10L, 50L, 80L, 100L, 120L, 133L, 333L);
        List<Long> resultList = sourceLongList.parallelStream().filter(greaterThan100).collect(Collectors.toList());

Если вы используете 3-й Пари библиотеки в порядке cyclops-react определяет ленивые расширенные коллекции с этой встроенной функциональностью. Например, мы могли бы просто написать

ListX myListToParse;

ListX myFinalList = myListToParse.фильтр (elt - > elt != недействительный) .карта (elt - > doSomething (elt));

myFinalList не оценивается до первого доступа (и там после того, как материализованный список кэшируется и повторно используемый.)

[раскрытие я ведущий разработчик cyclops-react]