Подтвердить что ты не робот

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

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

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

Это, конечно, не сложная задача сама по себе. Но мне интересно, следует ли использовать потоки или цикл for (-each).

Список

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

Пример - Stream

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(), то?

4b9b3361

Ответ 1

Ваше предположение верно. Реализация вашего потока медленнее, чем цикл for.

Это использование потока должно быть таким же быстрым, как и для цикла while:

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

Это повторяется через элементы, применяя String::toLowerCase и фильтр к элементам один за другим и завершая первый элемент, который соответствует.

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

Ответ 2

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

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

Использование .map(String::toLowerCase).anyMatch(path::contains) может сохранять циклы CPU и память, если у вас есть значительно большее количество элементов. Тем не менее, это преобразует каждый 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 позволяет вести себя так же, как обычные операции с строкой, но позволяет двигателю некоторое время готовиться, например. используя алгоритм Boyer Moore, чтобы быть более эффективным, когда дело доходит до фактического сравнения.

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

Ответ 3

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

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

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

Ответ 4

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

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

Существует хорошая статья о ForLoop, Stream и ParallelStream Производительность.

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