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

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

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

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

Способ 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()); 

Ответ 1

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

Способ 2 предпочтительнее, потому что

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

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

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

Ответ 2

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

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

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 - поэтому вы вынуждены использовать lambda с состоянием. Но это действительно плохая идея, если вы используете параллельные потоки.

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

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

Ответ 3

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

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

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

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

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

Ответ 4

Если вы используете Eclipse Collections, вы можете использовать метод 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);

Он с готовностью оценивает и должен быть немного быстрее, чем использование Stream.

Примечание: Я являюсь коммиттером для коллекций Eclipse.

Ответ 5

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

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

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

Ответ 6

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

Предполагая List<String>

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

с микромикровым эталоном, 1M-записями, 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}

Ответ 7

Может быть метод 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());

Ответ 8

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

ListX myListToParse;

ListX myFinalList = myListToParse.filter(elt → elt!= null)                                .map(elt → doSomething (elt));

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

[Раскрытие информации Я ведущий разработчик реакции циклопов]