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 ответов:
Не беспокойтесь о каких-либо различиях в производительности, они будут минимальными в этом случае обычно.
Метод 2 предпочтительнее, потому что
это не требует мутации коллекции, которая существует вне лямбда-выражения,
это более читабельно, потому что различные шаги, выполняемые в конвейере сбора, записываются последовательно (сначала операция фильтра, затем операция карты, а затем сбор результат), (для получения дополнительной информации о преимуществах коллекторных трубопроводов см. Martin Fowler отличная статья)
вы можете легко изменить способ сбора значений, заменив
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, должна быть
- не вмешивается, что означает, что функция не должна изменять источник потока, если он не является параллельным (например,
 ArrayList).- без избегайте неожиданных результатов при параллельной обработке (вызванной различиями в расписании потоков).
 еще одно преимущество второго подхода заключается в том, что если поток параллелен, а коллектор параллелен и неупорядочен, то эти характеристики могут предоставить полезные подсказки для операции сокращения для одновременного сбора.
Если вы используете Коллекции 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]