Pobierz ostatni element Stream / List w jednej linijce

118

Jak mogę uzyskać ostatni element strumienia lub listy w poniższym kodzie?

Gdzie data.careasjest 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?

Jak widać zdobycie pierwszego elementu, z pewnym filter, nie jest trudne.

Jednak uzyskanie ostatniego elementu w jednolinijce to prawdziwy ból:

  • Wygląda na to, że nie mogę go uzyskać bezpośrednio z pliku Stream. (Miałoby to sens tylko w przypadku skończonych strumieni)
  • Wydaje się również, że nie można dostać rzeczy, jak first()i last()z Listinterfejsem, który jest naprawdę uciążliwe.

Nie widzę żadnego argumentu za brakiem podania metody first()a last()w Listinterfejsie, ponieważ elementy tam są uporządkowane, a ponadto znany jest rozmiar.

Ale zgodnie z pierwotną odpowiedzią: jak uzyskać ostatni element skończonego Stream?

Osobiście jest to najbliższe, jakie mogłem uzyskać:

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

Jednak wymaga to użycia indexOfna każdym elemencie, co najprawdopodobniej nie jest ogólnie pożądane, ponieważ może to pogorszyć wydajność.

skiwi
źródło
10
Guava zapewnia, Iterables.getLastktóry używa Iterable, ale jest zoptymalizowany do pracy z List. Irytacją jest to, że nie ma getFirst. Ogólnie StreamAPI jest strasznie analne, pomijając wiele wygodnych metod. C # LINQ, przez constrast, z przyjemnością zapewnia, .Last()a nawet .Last(Func<T,Boolean> predicate), mimo że obsługuje również nieskończone Enumerables.
Aleksandr Dubinsky
@AleksandrDubinsky głosował za, ale jedna uwaga dla czytelników. StreamAPI nie jest w pełni porównywalne, LINQponieważ oba są wykonywane w zupełnie innym paradygmacie. Nie jest gorzej ani lepiej, jest po prostu inaczej. I zdecydowanie brakuje niektórych metod nie dlatego, że
twórcy
1
W przypadku prawdziwej jednolinijki ta nić może być przydatna.
kwantowy

Odpowiedzi:

185

Ostatni element można pobrać metodą Stream :: Redukcja . Poniższa lista zawiera minimalny przykład ogólnego przypadku:

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

Ta implementacja działa dla wszystkich uporządkowanych strumieni (w tym strumieni utworzonych z list ). W przypadku strumieni nieuporządkowanych z oczywistych powodów nie określono, który element zostanie zwrócony.

Implementacja działa zarówno dla strumieni sekwencyjnych, jak i równoległych . Na pierwszy rzut oka może to być zaskakujące i niestety dokumentacja nie podaje tego wprost. Jest to jednak ważna cecha strumieni i staram się to wyjaśnić:

  • Javadoc dla metody Stream :: redukuje stwierdza, że nie jest ograniczona do wykonywania sekwencyjnego .
  • Javadoc wymaga również, aby „funkcja akumulatora była asocjacyjną , niezakłócającą , bezstanową funkcją do łączenia dwóch wartości” , co oczywiście ma miejsce w przypadku wyrażenia lambda (first, second) -> second.
  • Dokument Javadoc dotyczący operacji redukcji stwierdza: „Klasy strumieni mają wiele form ogólnych operacji redukcyjnych, zwanych redukcją () i zbieraniem () [..]” oraz „prawidłowo skonstruowana operacja redukcji jest z natury równoległa , o ile funkcja (y ) używane do przetwarzania elementów są asocjacyjne i bezstanowe . ”

Dokumentacja dla blisko spokrewnionych Kolektorów jest jeszcze bardziej wyraźna: „Aby zapewnić, że wykonywanie sekwencyjne i równoległe daje równoważne wyniki , funkcje kolektora muszą spełniać ograniczenia dotyczące tożsamości i asocjatywności ”.


Wracając do pierwotnego pytania: poniższy kod przechowuje odwołanie do ostatniego elementu zmiennej lasti zgłasza wyjątek, jeśli strumień jest pusty. Złożoność jest liniowa w długości strumienia.

CArea last = data.careas
                 .stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .reduce((first, second) -> second).get();
nosid
źródło
Niezłe, dzięki! Czy przy okazji wiesz, czy możliwe jest pominięcie nazwy (na przykład za pomocą _lub podobnego) w przypadkach, gdy nie potrzebujesz parametru? Tak by było: .reduce((_, current) -> current)gdyby tylko taka składnia aws była prawidłowa.
skiwi
2
@skiwi możesz użyć dowolnej dozwolonej nazwy zmiennej, na przykład: .reduce(($, current) -> current)lub .reduce((__, current) -> current)(podwójne podkreślenie).
assylias
2
Technicznie może nie działać w przypadku żadnych strumieni. Dokumentacja, na którą wskazujesz, jak również Stream.reduce(BinaryOperator<T>)nie wspomina o tym, czy reduceprzestrzega rozkazu napotkania, a operacja terminalowa może zignorować kolejność spotkań, nawet jeśli strumień jest uporządkowany. Nawiasem mówiąc, słowo „przemienność” nie pojawia się w javadocach Stream, więc jego brak niewiele nam mówi.
Aleksandr Dubinsky,
2
@AleksandrDubinsky: Dokładnie, dokumentacja nie wspomina o przemienności , ponieważ nie ma to znaczenia dla operacji redukcji . Ważną częścią jest: „[…] Prawidłowo skonstruowana operacja redukcji jest z natury równoległa, o ile funkcje używane do przetwarzania elementów są asocjacyjne […]”.
nosid
2
@Aleksandr Dubinsky: oczywiście nie jest to „teoretyczna kwestia specyfikacji”. To robi różnicę między reduce((a,b)->b)byciem poprawnym rozwiązaniem dla uzyskania ostatniego elementu (oczywiście z uporządkowanego strumienia), czy nie. Oświadczenie Briana Goetza mówi o tym, dalej dokumentacja API stwierdza, że reduce("", String::concat)jest to nieefektywne, ale poprawne rozwiązanie dla konkatenacji ciągów, co oznacza utrzymanie kolejności spotkań. Zamiar jest dobrze znany, dokumentacja musi nadrobić zaległości.
Holger
42

Jeśli masz kolekcję (lub bardziej ogólnie, iterowalną), możesz użyć Google Guava

Iterables.getLast(myIterable)

jako poręczny oneliner.

Peti
źródło
1
I możesz łatwo przekonwertować strumień na Iterables.getLast(() -> data.careas.stream().filter(c -> c.bbox.orientationHorizontal).iterator())
iterowalny
10

Jeden liner (nie ma potrzeby streamowania;):

Object lastElement = list.get(list.size()-1);
nimo23
źródło
30
Jeśli lista jest pusta, ten kod zwróci ArrayIndexOutOfBoundsException.
Dragon,
8

Guava ma dedykowaną metodę dla tego przypadku:

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

Jest to odpowiednik, stream.reduce((a, b) -> b)ale twórcy twierdzą, że ma znacznie lepszą wydajność.

Z dokumentacji :

Czas wykonania tej metody będzie mieścić się w przedziale od O (log n) do O (n), co będzie działało lepiej w przypadku wydajnie dzielonych strumieni.

Warto wspomnieć, że jeśli strumień jest nieuporządkowany ta metoda zachowuje się jak findAny().

k13i
źródło
1
@ZhekaKozlov jakby ... Holger wykazała pewne wady z nim tutaj
Eugene
0

Jeśli chcesz uzyskać ostatnią liczbę N elementów. Można użyć zamknięcia. Poniższy kod utrzymuje zewnętrzną kolejkę o stałym rozmiarze do momentu, gdy strumień osiągnie koniec.

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

Inną opcją może być użycie operacji redukcji przy użyciu tożsamości jako kolejki.

    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);
Himanshu Ahire
źródło
-1

Możesz także użyć funkcji skip () jak poniżej ...

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

jest bardzo prosty w użyciu.

Parag Vaidya
źródło
Uwaga: nie powinieneś polegać na „pomijaniu” strumienia w przypadku ogromnych kolekcji (milionów wpisów), ponieważ „pomijanie” jest realizowane poprzez iterację wszystkich elementów, aż do osiągnięcia liczby N-tej. Spróbował tego. Byłem bardzo rozczarowany wydajnością w porównaniu z prostą operacją uzyskania indeksu.
java.is.for.desktop
1
również jeśli lista jest pusta, wyrzuciArrayIndexOutOfBoundsException
Jindra Vysocký