Найти первый элемент по предикату


504

Я только начал играть с лямбдами Java 8, и я пытаюсь реализовать некоторые вещи, к которым я привык в функциональных языках.

Например, большинство функциональных языков имеют какую-то функцию поиска, которая работает с последовательностями, или списки, которые возвращают первый элемент, для которого существует предикат true. Единственный способ добиться этого в Java 8 - это:

lst.stream()
    .filter(x -> x > 5)
    .findFirst()

Однако это кажется мне неэффективным, так как фильтр будет сканировать весь список, по крайней мере, насколько я понимаю (что может быть неправильно). Есть ли способ лучше?


53
Это не неэффективно, реализация Java 8 Stream оценивается лениво, поэтому фильтр применяется только для работы терминала. Тот же вопрос здесь: stackoverflow.com/questions/21219667/stream-and-lazy-evaluation
Марек Грегор

1
Круто. Это то, на что я надеялся. В противном случае это был бы главный провал дизайна.
Сики

2
Если вы действительно хотите проверить, содержит ли список такой элемент вообще (не выделяя первый из, возможно, нескольких), то .findAny () теоретически может быть более эффективным в настройке параллелла, и, конечно, сообщит это намерение более четко.
Иоахим Лоус

По сравнению с простым циклом forEach это создаст множество объектов в куче и десятки динамических вызовов методов. Хотя это может не всегда влиять на итоговые показатели в ваших тестах производительности, в горячих точках имеет смысл воздерживаться от тривиального использования Stream и подобных тяжеловесных конструкций.
Агостон Хорват

Ответы:


720

Нет, фильтр не сканирует весь поток. Это промежуточная операция, которая возвращает ленивый поток (фактически все промежуточные операции возвращают ленивый поток). Чтобы убедить вас, вы можете просто сделать следующий тест:

List<Integer> list = Arrays.asList(1, 10, 3, 7, 5);
int a = list.stream()
            .peek(num -> System.out.println("will filter " + num))
            .filter(x -> x > 5)
            .findFirst()
            .get();
System.out.println(a);

Какие выводы:

will filter 1
will filter 10
10

Вы видите, что фактически обрабатываются только два первых элемента потока.

Таким образом, вы можете пойти с вашим подходом, который прекрасно подходит.


37
Как примечание, я использовал get();здесь, потому что я знаю, какие значения я передаю в потоковый конвейер и, следовательно, что будет результат. На практике не следует использовать get();, но orElse()/ orElseGet()/ orElseThrow()(для более значимой ошибки вместо NSEE), поскольку вы можете не знать, приведут ли операции, примененные к потоковому конвейеру, к элементу.
Алексис С.

31
.findFirst().orElse(null);например
Гонди

20
Не используйте orElse null. Это должно быть анти-паттерном. Все это включено в Дополнительное, так почему вы должны рисковать NPE? Я думаю, что иметь дело с Optional - лучший способ. Просто протестируйте Optional с помощью isPresent (), прежде чем использовать его.
BeJay

@BeJay, я не понимаю. что я должен использовать вместо orElse?
Джон Хенкель

3
@JohnHenckel Я думаю, что BeJay означает, что вы должны оставить это как Optionalтип, что и .findFirstвозвращает. Одно из применений Optional - помочь разработчикам избежать необходимости иметь дело с nullseg вместо проверки myObject != null, вы можете проверить myOptional.isPresent()или использовать другие части интерфейса Optional. Это прояснило?
AMTerp

102

Однако это кажется мне неэффективным, так как фильтр будет сканировать весь список

Нет, не будет - он «сломается», как только будет найден первый элемент, удовлетворяющий предикату. Вы можете прочитать больше о лени в потоковом пакете javadoc , в частности (выделено мое):

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


5
Этот ответ был для меня более информативным и объясняет почему, а не только как. Я никогда не новые промежуточные операции всегда ленивы; Потоки Java продолжают удивлять меня.
Кевинарпе

30
return dataSource.getParkingLots()
                 .stream()
                 .filter(parkingLot -> Objects.equals(parkingLot.getId(), id))
                 .findFirst()
                 .orElse(null);

Мне пришлось отфильтровать только один объект из списка объектов. Так что я использовал это, надеюсь, это поможет.


ЛУЧШЕ: так как мы ищем логическое возвращаемое значение, мы можем сделать это лучше, добавив null check: return dataSource.getParkingLots (). Stream (). Filter (parkingLot -> Objects.equals (parkingLot.getId (), id)) .findFirst (). orElse (null)! = null;
Шридхар Бхат

1
@shreedharbhat Вам не нужно делать .orElse(null) != null. Вместо этого, использовать Факультативный API, .isPresentто есть .findFirst().isPresent().
AMTerp

@shreedharbhat прежде всего OP не искал логическое возвращаемое значение. Во-вторых, если бы они были, было бы чище написать.stream().map(ParkingLot::getId).anyMatch(Predicate.isEqual(id))
AjaxLeung

13

В дополнение к ответу Алексиса С. Если вы работаете со списком массивов, в котором вы не уверены, существует ли элемент, который вы ищете, используйте это.

Integer a = list.stream()
                .peek(num -> System.out.println("will filter " + num))
                .filter(x -> x > 5)
                .findFirst()
                .orElse(null);

Тогда вы могли бы просто проверить , является ли это .null


1
Вы должны исправить свой пример. Вы не можете присвоить null обычному int. stackoverflow.com/questions/2254435/can-an-int-be-null-in-java
RubioRic

Я отредактировал твой пост. 0 (ноль) может быть действительным результатом при поиске в списке целых чисел. Тип переменной заменен на Integer, а значение по умолчанию на null.
RubioRic

0

import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

// Stream is ~30 times slower for same operation...
public class StreamPerfTest {

    int iterations = 100;
    List<Integer> list = Arrays.asList(1, 10, 3, 7, 5);


    // 55 ms
    @Test
    public void stream() {

        for (int i = 0; i < iterations; i++) {
            Optional<Integer> result = list.stream()
                    .filter(x -> x > 5)
                    .findFirst();

            System.out.println(result.orElse(null));
        }
    }

    // 2 ms
    @Test
    public void loop() {

        for (int i = 0; i < iterations; i++) {
            Integer result = null;
            for (Integer walk : list) {
                if (walk > 5) {
                    result = walk;
                    break;
                }
            }
            System.out.println(result);
        }
    }
}

0

Улучшенный однострочный ответ: если вы ищете логическое возвращаемое значение, мы можем сделать это лучше, добавив isPresent:

return dataSource.getParkingLots().stream().filter(parkingLot -> Objects.equals(parkingLot.getId(), id)).findFirst().isPresent();

Если вы хотите логическое возвращаемое значение, вы должны использовать anyMatch
AjaxLeung
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.