Когда мне следует использовать потоки?

99

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

Например, у меня есть список, содержащий разные пути в разные места. Теперь я хотел бы проверить, содержит ли один заданный путь какой-либо из путей, указанных в списке. Я хотел бы вернуть в booleanзависимости от того, было ли выполнено условие.

Это, конечно, не сложная задача. Но мне интересно, следует ли мне использовать потоки или цикл for (-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параметр всегда в нижнем регистре .

Мое первое предположение заключается в том, что подход for-each быстрее, потому что цикл вернется немедленно, если условие будет выполнено. В то время как поток по-прежнему будет перебирать все записи списка, чтобы завершить фильтрацию.

Верно ли мое предположение? Если да, то почему (или, скорее, когда ) я должен использовать это stream()?

Mcuenez
источник
11
Потоки более выразительны и удобочитаемы, чем традиционные циклы for. В последнем случае вам нужно быть осторожным с внутренними особенностями if-then и условий и т. Д. Выражение потока очень четкое: преобразовать имена файлов в нижний регистр, затем отфильтровать по чему-то, а затем подсчитать, собрать и т. Д. Результат: очень итеративный выражение потока вычислений.
Жан-Батист Юнес,
12
Здесь нет необходимости new String[]{…}. Просто используйтеArrays.asList("my/path/one", "my/path/two")
Хольгер
4
Если ваш источник - a 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

Решение о том, использовать ли Streams или нет, должно определяться не соображениями производительности, а скорее удобочитаемостью. Когда дело доходит до производительности, есть и другие соображения.

С вашим .filter(path::contains).collect(Collectors.toList()).size() > 0подходом вы обрабатываете все элементы и собираете их во временный List, прежде чем сравнивать размер, тем не менее, это почти никогда не имеет значения для Stream, состоящего из двух элементов.

Использование .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флагом заставляет ее вести себя так же, как обычные строковые операции, но позволяет механизму потратить некоторое время на подготовку, например, используя алгоритм Бойера Мура, чтобы быть более эффективным, когда дело доходит до фактического сравнения.

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

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

Даже dummy/path/onerousсчитается отвечающим критериям, поскольку это containsстрока my/path/one

Хольгер
источник
Хорошие идеи о возможной оптимизации производительности, спасибо. Что касается последней части вашего ответа: если мой ответ на ваш комментарий не был удовлетворительным, рассматривайте мой пример кода как простой помощник для других, чтобы понять, о чем я прошу, а не как реальный код. Кроме того, вы всегда можете отредактировать вопрос, если у вас есть лучший пример.
mcuenez
3
Я понимаю ваш комментарий, что эта операция - это то, что вам действительно нужно, поэтому нет необходимости ее менять. Я просто оставлю последний раздел для будущих читателей, чтобы они знали, что это не типичная операция, но также, что она уже обсуждалась и не требует дальнейших комментариев…
Хольгер
На самом деле потоки идеально подходят для оптимизации памяти, когда объем рабочей памяти
превышает
21

Да. Ты прав. Ваш потоковый подход будет иметь некоторые накладные расходы. Но вы можете использовать такую ​​конструкцию:

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

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

rvit34
источник
3
Это anyMatchярлык для filter(...).findFirst().isPresent()?
mcuenez
6
Да, это так! Это даже лучше, чем мое первое предложение.
Стефан Прайс
8

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

Если нам нужна производительность, мы должны использовать parallelStream, для которого был разработан. Серийный вообще медленнее.

Есть хорошая статья, о которой стоит прочитать , а производительность ForLoopStreamParallelStream .

В вашем коде мы можем использовать методы завершения, чтобы остановить поиск при первом совпадении. (anyMatch ...)

Пауло Рикардо Алмейда
источник
5
Обратите внимание, что для небольших потоков и в некоторых других случаях параллельный поток может быть медленнее из-за стоимости запуска. И если у вас есть упорядоченная терминальная операция, а не неупорядоченная, распараллеливаемая, повторная синхронизация в конце.
CAD97,
0

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

Кайчэн Ху
источник