Trova il primo elemento per predicato


504

Ho appena iniziato a giocare con Java 8 lambdas e sto cercando di implementare alcune delle cose a cui sono abituato nei linguaggi funzionali.

Ad esempio, la maggior parte dei linguaggi funzionali ha un qualche tipo di funzione find che opera su sequenze o elenchi che restituiscono il primo elemento, per cui è il predicato true. L'unico modo che posso vedere per raggiungere questo obiettivo in Java 8 è:

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

Tuttavia, questo mi sembra inefficiente, poiché il filtro analizzerà l'intero elenco, almeno per quanto ne so (che potrebbe essere sbagliato). Esiste un modo migliore?


53
Non è inefficiente, l'implementazione di Java 8 Stream è valutata in modo pigro, quindi il filtro viene applicato solo al funzionamento del terminale. Stessa domanda qui: stackoverflow.com/questions/21219667/stream-and-lazy-evaluation
Marek Gregor

1
Freddo. Questo è quello che speravo potesse fare. Altrimenti sarebbe stato un grande flop di progettazione.
Siki,

2
Se la tua intenzione è davvero quella di verificare se l'elenco contenga un tale elemento (non individuare il primo di alcuni eventualmente diversi), .findAny () può teoricamente essere più efficiente in un contesto parallelo e, naturalmente, comunica tale intento in modo più chiaro.
Joachim Lous,

Rispetto a un semplice ciclo per ogni ciclo, ciò creerebbe molti oggetti nell'heap e dozzine di chiamate a metodi dinamici. Anche se questo potrebbe non influenzare sempre la linea di fondo nei test delle prestazioni, nei punti caldi fa la differenza astenersi dall'uso banale di Stream e di simili pesi massimi.
Agoston Horvath,

Risposte:


720

No, il filtro non esegue la scansione dell'intero flusso. È un'operazione intermedia, che restituisce un flusso pigro (in realtà tutte le operazioni intermedie restituiscono un flusso pigro). Per convincerti, puoi semplicemente fare il seguente 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);

Quali uscite:

will filter 1
will filter 10
10

Vedi che solo i primi due elementi del flusso vengono effettivamente elaborati.

Quindi puoi andare con il tuo approccio che è perfettamente bene.


37
Come nota, ho usato get();qui perché so quali valori fornisco alla pipeline del flusso e quindi che ci sarà un risultato. In pratica, non dovresti usare get();, ma orElse()/ orElseGet()/ orElseThrow()(per un errore più significativo invece di un NSEE) in quanto potresti non sapere se le operazioni applicate alla pipeline del flusso daranno origine a un elemento.
Alexis C.,

31
.findFirst().orElse(null);per esempio
Gondy,

20
Non usare orElse null. Dovrebbe essere un anti-schema. È tutto incluso nell'opzionale, quindi perché dovresti rischiare un NPE? Penso che trattare con Opzionale sia il modo migliore. Basta provare l'Opzionale con isPresent () prima di usarlo.
BeJay

@ BeJay non capisco. cosa dovrei usare invece di orElse?
John Henckel,

3
@JohnHenckel Penso che BeJay significhi che dovresti lasciarlo come un Optionaltipo, che è ciò che .findFirstritorna. Uno degli usi di Opzionale è aiutare gli sviluppatori a evitare di dover gestire nullseg invece di controllare myObject != null, è possibile controllare myOptional.isPresent()o utilizzare altre parti dell'interfaccia Opzionale. Ciò ha reso più chiaro?
AMTerp

102

Tuttavia, questo mi sembra inefficiente, poiché il filtro eseguirà la scansione dell'intero elenco

No non lo farà - si "spezzerà" non appena viene trovato il primo elemento che soddisfa il predicato. Puoi leggere di più sulla pigrizia nel pacchetto stream javadoc , in particolare (sottolineatura mia):

Molte operazioni di streaming, come filtraggio, mappatura o rimozione di duplicati, possono essere implementate pigramente, esponendo opportunità di ottimizzazione. Ad esempio, "trova la prima stringa con tre vocali consecutive" non è necessario esaminare tutte le stringhe di input. Le operazioni di flusso sono suddivise in operazioni intermedie (che producono flusso) e operazioni terminali (che producono valore o effetti collaterali). Le operazioni intermedie sono sempre pigre.


5
Questa risposta mi è stata più istruttiva e mi spiega perché, non solo come. Non ho mai nuove operazioni intermedie sempre pigre; I flussi Java continuano a sorprendermi.
Kevinevpe,

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

Ho dovuto filtrare solo un oggetto da un elenco di oggetti. Quindi l'ho usato, spero che sia d'aiuto.


MEGLIO: poiché stiamo cercando un valore di ritorno booleano, possiamo farlo meglio aggiungendo un controllo null: return dataSource.getParkingLots (). Stream (). Filter (parkingLot -> Objects.equals (parkingLot.getId (), id)) .findFirst (). orElse (null)! = null;
shreedhar bhat,

1
@shreedharbhat Non dovresti farlo .orElse(null) != null. Utilizza invece le API facoltative, ad .isPresentes .findFirst().isPresent().
AMTerp

@shreedharbhat OP prima di tutto non cercava un valore di ritorno booleano. Secondo, se lo fossero, sarebbe più pulito scrivere.stream().map(ParkingLot::getId).anyMatch(Predicate.isEqual(id))
AjaxLeung

13

Oltre alla risposta di Alexis C , se stai lavorando con un elenco di array, in cui non sei sicuro che l'elemento che stai cercando esista, usa questo.

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

Allora si potrebbe semplicemente verificare se un è null.


1
Dovresti risolvere il tuo esempio. Non è possibile assegnare null a un int int. stackoverflow.com/questions/2254435/can-an-int-be-null-in-java
RubioRic

Ho modificato il tuo post. 0 (zero) può essere un risultato valido quando si cerca in un elenco di numeri interi. Sostituito il tipo di variabile per intero e il valore predefinito per 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

Risposta One-Liner migliorata: se stai cercando un valore di ritorno booleano, possiamo farlo meglio aggiungendo isPresent:

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

Se si desidera un valore di ritorno booleano, è necessario utilizzare anyMatch
AjaxLeung
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.