Znajdź pierwszy element według predykatu


504

Właśnie zacząłem grać z lambdami Java 8 i próbuję zaimplementować niektóre rzeczy, do których jestem przyzwyczajony w językach funkcjonalnych.

Na przykład większość języków funkcjonalnych ma funkcję wyszukiwania, która działa na sekwencjach lub listach zwracających pierwszy element, dla którego jest predykat true. Jedyny sposób, w jaki mogę to osiągnąć w Javie 8, to:

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

Wydaje mi się to jednak nieefektywne, ponieważ filtr skanuje całą listę, przynajmniej według mojego zrozumienia (co może być błędne). Czy jest lepszy sposób?


53
To nie jest nieefektywne, implementacja Java 8 Stream jest leniwa, więc filtr jest stosowany tylko do działania terminala. To samo pytanie tutaj: stackoverflow.com/questions/21219667/stream-and-lazy-evaluation
Marek Gregor

1
Fajne. Tak miałem nadzieję. W przeciwnym razie byłby to duży projekt.
siki

2
Jeśli naprawdę chcesz sprawdzić, czy lista w ogóle zawiera taki element (nie wyodrębniając pierwszego z kilku możliwych), .findAny () może teoretycznie być bardziej wydajny w ustawieniu równoległym i oczywiście przekazuje ten zamiar jaśniej.
Joachim Lous,

W porównaniu z prostym cyklem forEach stworzyłoby to wiele obiektów na stercie i dziesiątki dynamicznych wywołań metod. Chociaż może to nie zawsze wpływać na wynik końcowy w testach wydajności, w gorących punktach odmawia się trywialnego użycia Stream i podobnych ciężkich konstrukcji.
Agoston Horvath,

Odpowiedzi:


720

Nie, filtr nie skanuje całego strumienia. Jest to operacja pośrednia, która zwraca leniwy strumień (właściwie wszystkie operacje pośrednie zwracają leniwy strumień). Aby Cię przekonać, możesz po prostu wykonać następujący test:

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);

Które wyjścia:

will filter 1
will filter 10
10

Widzisz, że tylko dwa pierwsze elementy strumienia są faktycznie przetwarzane.

Możesz więc zastosować swoje podejście, które jest całkowicie w porządku.


37
Jako uwagę użyłem get();tutaj, ponieważ wiem, które wartości przesyłam do potoku strumienia, a zatem że będzie wynik. W praktyce nie powinieneś używać get();, ale orElse()/ orElseGet()/ orElseThrow()(dla bardziej znaczącego błędu zamiast NSEE), ponieważ możesz nie wiedzieć, czy operacje zastosowane do potoku strumienia spowodują powstanie elementu.
Alexis C.

31
.findFirst().orElse(null);na przykład
Gondy

20
Nie używaj orElse null. To powinien być anty-wzór. Wszystko to jest zawarte w Opcjonalnym, więc dlaczego warto ryzykować NPE? Myślę, że radzenie sobie z Opcjonalnym jest lepszym sposobem. Po prostu przetestuj Opcjonalne za pomocą isPresent () przed użyciem.
BeJay

@BeJay nie rozumiem. czego powinienem użyć zamiast orElse?
John Henckel,

3
@JohnHenckel Myślę, że BeJay oznacza, że ​​powinieneś zostawić go jako Optionaltyp, który .findFirstpowraca. Jednym z zastosowań Opcjonalnych jest pomoc deweloperom w unikaniu konieczności radzenia sobie z nullseg zamiast sprawdzania myObject != null, możesz sprawdzać myOptional.isPresent()lub używać innych części interfejsu opcjonalnego. Czy to wyjaśniło?
AMTerp

102

Wydaje mi się to jednak nieefektywne, ponieważ filtr skanuje całą listę

Nie, nie zrobi tego - „pęknie”, gdy tylko zostanie znaleziony pierwszy element spełniający predykat. Możesz przeczytać więcej o lenistwie w pakiecie strumieniowym javadoc , w szczególności (wyróżnienie moje):

Wiele operacji przesyłania strumieniowego, takich jak filtrowanie, mapowanie lub usuwanie duplikatów, można wdrożyć leniwie, co daje możliwości optymalizacji. Na przykład „znajdź pierwszy ciąg z trzema kolejnymi samogłoskami” nie musi badać wszystkich ciągów wejściowych. Operacje strumieniowe dzielą się na operacje pośrednie (wytwarzające strumień) i operacje końcowe (wytwarzające wartość lub powodujące skutki uboczne). Operacje pośrednie są zawsze leniwe.


5
Ta odpowiedź była dla mnie bardziej pouczająca i wyjaśnia dlaczego, nie tylko jak. Nigdy nie nowe operacje pośrednie są zawsze leniwe; Strumienie Java nadal mnie zaskakują.
kevinarpe

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

Musiałem odfiltrować tylko jeden obiekt z listy obiektów. Więc użyłem tego, mam nadzieję, że to pomoże.


LEPSZE: ponieważ szukamy zwracanej wartości logicznej, możemy to zrobić lepiej, dodając kontrolę zerową: return dataSource.getParkingLots (). Stream (). Filter (parkingLot -> Objects.equals (parkingLot.getId (), id)) .findFirst (). orElse (null)! = null;
shreedhar bhat

1
@shreedharbhat Nie powinieneś tego robić .orElse(null) != null. Zamiast tego skorzystaj z opcjonalnych interfejsów API, .isPresenttj .findFirst().isPresent().
AMTerp

@shreedharbhat po pierwsze OP nie szukał logicznej wartości zwracanej. Po drugie, gdyby tak było, pisanie byłoby czystsze.stream().map(ParkingLot::getId).anyMatch(Predicate.isEqual(id))
AjaxLeung

13

Oprócz odpowiedzi Alexis C: Jeśli pracujesz z listą tablic, w której nie masz pewności, czy element, którego szukasz, istnieje, użyj tego.

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

Następnie można po prostu sprawdzić, czy jest .null


1
Powinieneś naprawić swój przykład. Nie możesz przypisać null do zwykłego int. stackoverflow.com/questions/2254435/can-an-int-be-null-in-java
RubioRic

Zredagowałem twój post. 0 (zero) może być prawidłowym wynikiem podczas wyszukiwania na liście liczb całkowitych. Typ zmiennej zamieniony na Integer, a wartość domyślna na 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

Ulepszona odpowiedź One-Liner: jeśli szukasz logicznej wartości zwracanej, możemy to zrobić lepiej, dodając isPresent:

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

Jeśli chcesz zwrócić wartość logiczną, powinieneś użyć anyMatch
AjaxLeung
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.