it-swarm.com.ru

Java 8 - лучший способ преобразовать список: карта или 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()); 
146
Emilien Brigand

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

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

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

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

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

124
herman

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

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

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

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

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

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

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

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

5
Mika'il

Если вы используете 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);

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

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

4
Craig P. Motlin

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

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

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

1
Eran

Существует третий вариант - использование 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}
0
harshtuna

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

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

ListX myListToParse;

ListX myFinalList = myListToParse.filter (elt -> elt! = Null) .map (elt -> doSomething (elt)); 

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

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

0
John McClean