Ottieni l'ultimo elemento di Stream / List in una riga


118

Come posso ottenere l'ultimo elemento di uno stream o di un elenco nel codice seguente?

Dov'è data.careasun List<CArea>:

CArea first = data.careas.stream()
                  .filter(c -> c.bbox.orientationHorizontal).findFirst().get();

CArea last = data.careas.stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .collect(Collectors.toList()).; //how to?

Come puoi vedere ottenere il primo elemento, con un certo filter, non è difficile.

Tuttavia, ottenere l'ultimo elemento in una battuta è un vero dolore:

  • Sembra che non sia possibile ottenerlo direttamente da un file Stream. (Avrebbe senso solo per flussi finiti)
  • Sembra inoltre che non è possibile ottenere le cose come first()e last()dalla Listinterfaccia, che è davvero un dolore.

Non vedo alcun argomento per non fornire un metodo first()e nell'interfaccia, poiché gli elementi in essa sono ordinati e inoltre la dimensione è nota.last()List

Ma secondo la risposta originale: come ottenere l'ultimo elemento di un finito Stream?

Personalmente, questo è il massimo che potrei ottenere:

int lastIndex = data.careas.stream()
        .filter(c -> c.bbox.orientationHorizontal)
        .mapToInt(c -> data.careas.indexOf(c)).max().getAsInt();
CArea last = data.careas.get(lastIndex);

Tuttavia implica l'uso di un indexOfsu ogni elemento, che molto probabilmente non si desidera in genere in quanto può compromettere le prestazioni.


10
Guava fornisce Iterables.getLastche accetta Iterable ma è ottimizzato per funzionare List. Un problema domestico è che non ha getFirst. L' StreamAPI in generale è orribilmente anale, omettendo molti metodi di convenienza. LINQ di C #, per contrasto, è felice di fornire .Last()e persino .Last(Func<T,Boolean> predicate), anche se supporta anche Enumerable infiniti.
Aleksandr Dubinsky

@AleksandrDubinsky votato positivamente, ma una nota per i lettori. StreamL'API non è completamente paragonabile a LINQpoiché entrambe sono realizzate in un paradigma molto diverso. Non è peggio o meglio, è solo diverso. E sicuramente alcuni metodi sono assenti non perché gli sviluppatori di Oracle siano incompetenti o cattivi :)
Fasth

1
Per un vero one-liner, questo thread può essere utile.
quantum

Risposte:


185

È possibile ottenere l'ultimo elemento con il metodo Stream :: reduce . Il seguente elenco contiene un esempio minimo per il caso generale:

Stream<T> stream = ...; // sequential or parallel stream
Optional<T> last = stream.reduce((first, second) -> second);

Questa implementazione funziona per tutti i flussi ordinati (inclusi i flussi creati dagli elenchi ). Per i flussi non ordinati è per ovvi motivi non specificato quale elemento verrà restituito.

L'implementazione funziona sia per flussi sequenziali che paralleli . Ciò potrebbe essere sorprendente a prima vista e sfortunatamente la documentazione non lo afferma esplicitamente. Tuttavia, è una caratteristica importante degli stream e cerco di chiarirla:

  • Il Javadoc per il metodo Stream :: reduce afferma che " non è vincolato all'esecuzione sequenziale " .
  • Il Javadoc richiede anche che la "funzione accumulatore deve essere una funzione associativa , non interferente , senza stato per combinare due valori" , che è ovviamente il caso dell'espressione lambda (first, second) -> second.
  • Il Javadoc per le operazioni di riduzione afferma: "Le classi di flussi hanno più forme di operazioni di riduzione generali, chiamate reduce () e collect () [..]" e "un'operazione di riduzione costruita in modo appropriato è intrinsecamente parallelizzabile , fintanto che le funzioni ) utilizzati per elaborare gli elementi sono associativi e apolidi . "

La documentazione per i collector strettamente correlati è ancora più esplicita: "Per garantire che le esecuzioni sequenziali e parallele producano risultati equivalenti , le funzioni del collector devono soddisfare un'identità e un vincolo di associatività ".


Torna alla domanda originale: il codice seguente memorizza un riferimento all'ultimo elemento nella variabile laste genera un'eccezione se il flusso è vuoto. La complessità è lineare nella lunghezza del flusso.

CArea last = data.careas
                 .stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .reduce((first, second) -> second).get();

Bella, grazie! Sapete a proposito se è possibile omettere un nome (magari usando un _o simile) nei casi in cui non serve un parametro? Quindi sarebbe: .reduce((_, current) -> current)se solo fosse una sintassi valida.
skiwi

2
@skiwi puoi usare qualsiasi nome di variabile legale, ad esempio: .reduce(($, current) -> current)o .reduce((__, current) -> current)(doppio trattino basso).
assylias

2
Tecnicamente, potrebbe non funzionare per nessun flusso. La documentazione a cui si punta, così come per Stream.reduce(BinaryOperator<T>)non fa menzione se reduceobbedisce all'ordine di incontro, e un'operazione del terminale è libera di ignorare l'ordine di incontro anche se lo stream è ordinato. Per inciso, la parola "commutativa" non appare nei javadoc Stream, quindi la sua assenza non ci dice molto.
Aleksandr Dubinsky

2
@AleksandrDubinsky: Esatto, la documentazione non menziona la commutativa , perché non è rilevante per l' operazione di riduzione . La parte importante è: "[..] Un'operazione di riduzione correttamente costruita è intrinsecamente parallelizzabile, a condizione che le funzioni utilizzate per elaborare gli elementi siano associative [..]."
nosid

2
@Aleksandr Dubinsky: ovviamente, non è una "questione teorica delle specifiche". Fa la differenza tra reduce((a,b)->b)essere una soluzione corretta per ottenere l'ultimo elemento (di un flusso ordinato, ovviamente) o meno. L'affermazione di Brian Goetz fa un punto, inoltre la documentazione API afferma che reduce("", String::concat)è una soluzione inefficiente ma corretta per la concatenazione di stringhe, il che implica il mantenimento dell'ordine di incontro. L'intenzione è ben nota, la documentazione deve recuperare.
Holger

42

Se hai una Collection (o più in generale un Iterable) puoi utilizzare Google Guava's

Iterables.getLast(myIterable)

come pratico oneliner.


1
E puoi facilmente convertire un flusso in un iterabile:Iterables.getLast(() -> data.careas.stream().filter(c -> c.bbox.orientationHorizontal).iterator())
shmosel

10

Un liner (non necessita di stream;):

Object lastElement = list.get(list.size()-1);

30
Se l'elenco è vuoto, verrà generato questo codice ArrayIndexOutOfBoundsException.
Dragon

8

Guava ha un metodo dedicato per questo caso:

Stream<T> stream = ...;
Optional<T> lastItem = Streams.findLast(stream);

È equivalente a, stream.reduce((a, b) -> b)ma i creatori affermano che ha prestazioni molto migliori.

Dalla documentazione :

Il runtime di questo metodo sarà compreso tra O (log n) e O (n), con prestazioni migliori su flussi suddivisibili in modo efficiente.

Vale la pena ricordare che se il flusso non è ordinato questo metodo si comporta come findAny().


1
@ZhekaKozlov una specie di ... Holger ha mostrato alcuni difetti qui
Eugene

0

Se è necessario ottenere l'ultimo numero N di elementi. La chiusura può essere utilizzata. Il codice seguente mantiene una coda esterna di dimensioni fisse fino a quando il flusso non raggiunge la fine.

    final Queue<Integer> queue = new LinkedList<>();
    final int N=5;
    list.stream().peek((z) -> {
        queue.offer(z);
        if (queue.size() > N)
            queue.poll();
    }).count();

Un'altra opzione potrebbe essere quella di ridurre l'operazione utilizzando l'identità come coda.

    final int lastN=3;
    Queue<Integer> reduce1 = list.stream()
    .reduce( 
        (Queue<Integer>)new LinkedList<Integer>(), 
        (m, n) -> {
            m.offer(n);
            if (m.size() > lastN)
               m.poll();
            return m;
    }, (m, n) -> m);

    System.out.println("reduce1 = " + reduce1);

-1

Puoi anche usare la funzione skip () come di seguito ...

long count = data.careas.count();
CArea last = data.careas.stream().skip(count - 1).findFirst().get();

è semplicissimo da usare.


Nota: non dovresti fare affidamento sul "salta" del flusso quando hai a che fare con raccolte enormi (milioni di voci), perché "salta" viene implementato iterando attraverso tutti gli elementi fino a raggiungere l'ennesimo numero. Provato. Sono rimasto molto deluso dalle prestazioni, rispetto a una semplice operazione di get-by-index.
java.is.for.desktop

1
anche se la lista è vuota, lanceràArrayIndexOutOfBoundsException
Jindra Vysocký
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.