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

188

У меня есть список, в 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()); 
Эмильен Бандит
источник
55
Второй. Правильная функция не должна иметь побочных эффектов, в первой реализации вы изменяете внешний мир.
Спасибо за все TheFish
37
просто вопрос стиля, но elt -> elt != nullего можно заменить наObjects::nonNull
the8472
2
@ the8472 Еще лучше было бы убедиться, что в коллекции нет нулевых значений, и использовать Optional<T>вместо этого в сочетании с flatMap.
Герман,
2
@SzymonRoziewski, не совсем. Для чего-то столь же тривиального, как это, работа, необходимая для настройки параллельного потока под капотом, заставит использовать эту конструкцию без звука.
МК
2
Обратите внимание, что вы можете написать, .map(this::doSomething)предполагая, что doSomethingэто нестатический метод. Если он статический, вы можете заменить thisего именем класса.
Герман

Ответы:

153

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

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

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

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

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

Герман
источник
43

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

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

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()); 
assylias
источник
1
Что касается производительности, в вашем случае «map» действительно выигрывает у «forEach», если вы используете parallelStreams. Мои тесты в миллисекундах: SO28319064.forEach: 187,310 ± 1,768 мс / опера - SO28319064.map: 189,180 ± 1,692 мс / опера --SO28319064.mapParallelStream: 55,577 ± 0,782 мс / опера
Джузеппе Бертоне
2
@GiuseppeBertone, дело до ассилий, но, на мой взгляд, ваша редакция противоречит первоначальному замыслу автора. Если вы хотите добавить свой собственный ответ, лучше добавить его, а не редактировать существующий. Также теперь ссылка на микробенчмарк не имеет отношения к результатам.
Тагир Валеев
5

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

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

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

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

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

MK
источник
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);

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

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

Крейг П. Мотлин
источник
1

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

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

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

Эран
источник
Я не уверен, что это правильный совет: он мог бы использовать forEachOrderedвместо того, чтобы, forEachесли он хотел использовать параллельный поток, но все же сохранить порядок. Но как документация для forEachгосударств, сохранение порядка встреч жертвует преимуществом параллелизма. Я подозреваю, что это также имеет место с toListтогда.
herman
0

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

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

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

ListX myListToParse;

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

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

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

Джон МакКлин
источник