Коли я повинен використовувати потоки?


99

Я просто натрапив на питання, коли використовував a Listта його stream()метод. Хоча я знаю, як ними користуватися, я не зовсім впевнений, коли ними користуватися.

Наприклад, у мене є список, який містить різні шляхи до різних місць. Тепер я хотів би перевірити, чи містить один заданий шлях будь-який із зазначених у списку шляхів. Я хотів би повернутися на booleanоснові того, чи було виконано умову чи ні.

Це, звичайно, не є складним завданням. Але мені цікаво, чи слід використовувати потоки чи цикл для (-each).

Список

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});

Приклад - Потік

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

Приклад - для кожного циклу

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if(path.contains(excludePath.toLowerCase())){
            return true;
        }
    }
    return false;
}

Зауважте, що pathпараметр завжди малий .

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

Чи правильне моє припущення? Якщо так, то чому (а точніше, коли ) я б stream()тоді користувався?


11
Потоки є більш виразними та читаними, ніж традиційні для циклів. Надалі вам слід бути обережними щодо властивостей if-then та умов тощо. Вираз потоку дуже чіткий: перетворіть назви файлів у малі регістри, потім щось фільтруйте, а потім підраховуйте, збирайте тощо результат: дуже ітеративний вираження потоку обчислень.
Жан-Батист Юньєс

12
Тут немає потреби new String[]{…}. Просто використовуйтеArrays.asList("my/path/one", "my/path/two")
Holger

4
Якщо ваш джерело є String[], дзвонити не потрібно Arrays.asList. Ви можете просто передати потоковий масив за допомогою Arrays.stream(array). До речі, у мене взагалі є труднощі з розумінням мети isExcludedтесту. Чи справді цікаво, чи елемент EXCLUDE_PATHSбуквально міститься десь у межах шляху? Тобто isExcluded("my/path/one/foo/bar/baz")повернеться true, як і isExcluded("foo/bar/baz/my/path/one/")
Холгер

3
Чудово, я не знав про Arrays.streamметод, дякую, що вказав на це. Дійсно, приклад, який я розмістив, здається абсолютно марним для інших, окрім мене. Я знаю про поведінку isExcludedметоду, але це дійсно просто щось, що мені потрібно для себе, таким чином, щоб відповісти на ваше запитання: так , це цікаво з причин, які я хотів би не згадувати, оскільки це не вкладалося б у сферу застосування оригінального питання.
mcuenez

1
Чому toLowerCaseзастосовується до константи, яка вже є малою? Чи не слід це застосовувати до pathаргументу?
Себастьян Редл

Відповіді:


78

Ваше припущення правильне. Реалізація потоку відбувається повільніше, ніж цикл for.

Це використання потоку має бути таким же швидким, як і цикл for-циклу:

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);

Це повторюється через елементи, додавання String::toLowerCaseта фільтр до елементів по одному та закінчення на першому, що відповідає.

Обидва collect()& anyMatch()є термінальними операціями. anyMatch()Виходить на першому знайденому елементі, хоча collect()вимагає обробки всіх елементів.


2
Дивовижний, не знав про findFirst()в поєднанні з filter(). Мабуть, я не знаю, як правильно використовувати потоки, як я думав.
mcuenez

4
У Інтернеті є кілька справді цікавих статей та презентацій щодо продуктивності API потоку, які мені здаються дуже корисними для розуміння того, як цей матеріал працює під кришкою. Я напевно можу порекомендувати трохи дослідити, якщо вам це цікаво.
Стефан Пріес

Після Вашої редагування я відчуваю, що Вашу відповідь слід прийняти, як Ви також відповіли на моє запитання у коментарях іншої відповіді. Хоча я хотів би надати @ rvit34 деяку заслугу за розміщення коду :-)
mcuenez

34

Рішення про те, чи використовувати потоки чи ні, не повинно визначатися врахуванням продуктивності, а скоріше читабельністю. Коли це дійсно стосується продуктивності, є й інші міркування.

Зі своїм .filter(path::contains).collect(Collectors.toList()).size() > 0підходом ви обробляєте всі елементи і збираєте їх у тимчасові List, перш ніж порівнювати розмір, але це навряд чи має значення для Потоку, що складається з двох елементів.

Використання .map(String::toLowerCase).anyMatch(path::contains)дозволяє зберегти цикли процесора та пам'ять, якщо у вас значно більша кількість елементів. Але все це перетворює кожне Stringна мале представлення, поки не буде знайдено відповідність. Очевидно, є сенс у використанні

private static final List<String> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .collect(Collectors.toList());

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}

замість цього. Тому вам не доведеться повторювати перетворення у малі регістри в кожному виклику isExcluded. Якщо кількість елементів у EXCLUDE_PATHSдовжинах рядків або їх довжина стає дійсно великою, ви можете розглянути можливість використання

private static final List<Predicate<String>> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
          .collect(Collectors.toList());

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}

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

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

До речі, наведені вище приклади коду зберігають логіку вашого початкового коду, що для мене виглядає сумнівно. Ваш isExcludedметод повертається true, якщо вказаний шлях містить будь-який з елементів у списку, тому він повертається trueдля /some/prefix/to/my/path/one, а також my/path/one/and/some/suffixабо навіть /some/prefix/to/my/path/one/and/some/suffix.

Навіть dummy/path/onerousвважається виконанням критеріїв, оскільки це containsрядок my/path/one...


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

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

Насправді потоки ідеально підходять для оптимізації пам’яті, коли обсяг робочої пам’яті перевищує ліміт сервера
ColacX

21

Так. Ти правий. Ваш підхід до потоку матиме деякі витрати. Але ви можете використовувати таку конструкцію:

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}

Основна причина використання потоків полягає в тому, що вони роблять ваш код більш простим і легким для читання.


3
Це anyMatchярлик filter(...).findFirst().isPresent()?
mcuenez

6
Так! Це навіть краще, ніж моя перша пропозиція.
Стефан Приєць

8

Мета потоків на Java - спростити складність написання паралельного коду. Це натхнене функціональним програмуванням. Серійний потік - це просто зробити код чистішим.

Якщо ми хочемо продуктивності, ми повинні використовувати паралельнуStream, яку було розроблено. Серійний, загалом, повільніше.

Існує гарна стаття , щоб читати про , і продуктивність . ForLoopStreamParallelStream

У вашому коді ми можемо використовувати методи припинення, щоб зупинити пошук у першій відповідності. (anyMatch ...)


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

0

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

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