Java 8 - Найкращий спосіб перетворення списку: карта чи передбачити?


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
Другий. Належна функція не повинна мати побічних ефектів. У першій реалізації ви модифікуєте зовнішній світ.
ДякуюForAllTheFish

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 - це термінальний метод - він повертає недійсність - тому ви змушені використовувати велику лямбду. Але це дійсно погана ідея, якщо ви використовуєте паралельні потоки .

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

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

1
Щодо продуктивності, у вашому випадку "map" дійсно перемагає "forEach", якщо ви використовуєте paralStreams. Мої показники в мілісекундах: SO28319064.для кожного: 187,310 ± 1,768 мс / оп - SO28319064.карта: 189,180 ± 1,692 мс / оп --SO28319064.mapParallelStream: 55,577 ± 0,782 мс / оп
Джузеппе Бертоне,

2
@GiuseppeBertone, це залежить від assylias, але, на мою думку, ваша редакція суперечить оригінальним намірам автора. Якщо ви хочете додати власну відповідь, краще додати її, а не редагувати існуючу. Крім того, тепер посилання на мікро-орієнтир не має відношення до результатів.
Тагір Валєєв

5

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

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

Другий спосіб є кращим з точки зору функціонального програмування, оскільки функція карти може приймати лямбда-функції без стану. Більш чітко, лямбда, передана функції функції карти, повинна бути

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

Інша перевага при другому підході полягає в тому, що якщо потік паралельний, а колектор паралельний і не упорядкований, то ці характеристики можуть дати корисні підказки операції зменшення, щоб робити збір одночасно.


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.


1

Я віддаю перевагу другий спосіб.

Якщо ви використовуєте перший спосіб, якщо ви вирішили використовувати паралельний потік для підвищення продуктивності, ви не будете контролювати порядок, в якому елементи будуть додані до списку вихідних даних forEach.

При використанні toListAPI Streams збереже порядок, навіть якщо ви використовуєте паралельний потік.


Я не впевнений, що це правильна порада: він міг використовувати forEachOrderedзамість цього, forEachякщо хотів би використовувати паралельний потік, але все-таки зберігати порядок. Але як документація для forEachдержав, збереження порядку зустрічей приносить користь паралелізму. Я підозрюю, що це теж і toListтоді.
герман

0

Є й третій варіант - використання stream().toArray()- перегляньте коментарі, чому у потоці не було методу toList . Це виявляється повільніше, ніж forEach () або збирати (), і менш виразним. Це може бути оптимізовано в наступних побудовах 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

Може бути метод 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 не оцінюється до першого доступу (і там після матеріалізованого списку кешується і повторно використовується).

[Розкриття інформації Я є провідним розробником циклоп-реакції]

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.