Holen Sie sich das letzte Element von Stream / List in einem Einzeiler

117

Wie kann ich das letzte Element eines Streams oder einer Liste im folgenden Code abrufen?

Wo data.careasist ein 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?

Wie Sie sehen können filter, ist es nicht schwer , das erste Element mit einem bestimmten zu bekommen .

Das letzte Element in einem Einzeiler zu bekommen, ist jedoch ein echtes Problem:

  • Es scheint, ich kann es nicht direkt von a erhalten Stream. (Es wäre nur für endliche Ströme sinnvoll)
  • Es scheint auch, dass Sie Dinge wie first()und last()von der ListBenutzeroberfläche nicht bekommen können, was wirklich ein Schmerz ist.

Ich sehe kein Argument dafür, dass in der Schnittstelle keine first()und- last()Methode angegeben Listwird, da die Elemente dort geordnet sind und außerdem die Größe bekannt ist.

Aber gemäß der ursprünglichen Antwort: Wie erhält man das letzte Element eines Endlichen Stream?

Persönlich ist dies der nächste, den ich bekommen könnte:

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

Es beinhaltet jedoch die Verwendung eines indexOfElements für jedes Element, was Sie im Allgemeinen höchstwahrscheinlich nicht möchten, da es die Leistung beeinträchtigen kann.

Skiwi
quelle
10
Guava bietet, Iterables.getLastwas Iterable nimmt, aber für die Arbeit optimiert ist List. Ein Haustier ärgern ist, dass es nicht hat getFirst. Die StreamAPI ist im Allgemeinen schrecklich anal und lässt viele bequeme Methoden aus. C # 's LINQ liefert gerne .Last()und sogar .Last(Func<T,Boolean> predicate), obwohl es auch unendlich viele Enumerables unterstützt.
Aleksandr Dubinsky
@AleksandrDubinsky positiv bewertet, aber eine Anmerkung für die Leser. StreamAPI ist nicht vollständig vergleichbar mitLINQ da beide in einem sehr unterschiedlichen Paradigma durchgeführt werden. Es ist nicht schlechter oder besser, es ist einfach anders. Und definitiv fehlen einige Methoden, nicht weil Orakelentwickler inkompetent oder gemein sind :)
fasth
1
Für einen echten Einzeiler kann dieser Faden von Nutzen sein.
Quanten

Antworten:

184

Es ist möglich, das letzte Element mit der Methode Stream :: redu zu erhalten . Die folgende Auflistung enthält ein minimales Beispiel für den allgemeinen Fall:

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

Diese Implementierung funktioniert für alle geordneten Streams (einschließlich aus Listen erstellter Streams ). Bei ungeordneten Streams ist aus offensichtlichen Gründen nicht angegeben, welches Element zurückgegeben wird.

Die Implementierung funktioniert sowohl für sequentielle als auch für parallele Streams . Das mag auf den ersten Blick überraschend sein, und in der Dokumentation wird dies leider nicht explizit angegeben. Es ist jedoch ein wichtiges Merkmal von Streams, und ich versuche es zu klären:

  • Das Javadoc für die Methode Stream :: Reduce gibt an, dass es " nicht auf die sequentielle Ausführung beschränkt ist " .
  • Das Javadoc verlangt auch, dass die "Akkumulatorfunktion eine assoziative sein muss , nicht störende , zustandslose Funktion zum Kombinieren zweier Werte sein muss" , was offensichtlich für den Lambda-Ausdruck der Fall ist(first, second) -> second.
  • Im Javadoc für Reduktionsoperationen heißt es: "Die Streams-Klassen haben mehrere Formen allgemeiner Reduktionsoperationen, die als redu () und collect () [..] bezeichnet werden" und "eine ordnungsgemäß konstruierte Reduktionsoperation ist von Natur aus parallelisierbar , solange die Funktion (en) ), die zur Verarbeitung der Elemente verwendet werden, sind assoziativ und zustandslos . "

Die Dokumentation für die eng verwandten Collectors ist noch expliziter: "Um sicherzustellen, dass sequentielle und parallele Ausführungen erzeugen gleichwertige Ergebnisse erzielt werden , müssen die Kollektor Funktionen eine Identität erfüllen und eine Assoziativität Zwänge.“


Zurück zur ursprünglichen Frage: Der folgende Code speichert einen Verweis auf das letzte Element in der Variablen lastund löst eine Ausnahme aus, wenn der Stream leer ist. Die Komplexität ist in der Länge des Stroms linear.

CArea last = data.careas
                 .stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .reduce((first, second) -> second).get();
nosid
quelle
Schön, danke! Wissen Sie übrigens, ob es möglich ist, einen Namen wegzulassen (möglicherweise durch Verwendung eines _oder eines ähnlichen), wenn Sie keinen Parameter benötigen? Also wäre: .reduce((_, current) -> current)wenn nur das eine gültige Syntax ist.
Skiwi
2
@skiwi Sie können einen beliebigen Namen für eine legale Variable verwenden, zum Beispiel: .reduce(($, current) -> current)oder .reduce((__, current) -> current)(doppelter Unterstrich).
Assylias
2
Technisch funktioniert es möglicherweise nicht für Streams. In der Dokumentation, auf die Sie verweisen, sowie für Stream.reduce(BinaryOperator<T>)wird nicht erwähnt, ob reducedie Reihenfolge der Begegnungen eingehalten wird, und bei einer Terminaloperation kann die Reihenfolge der Begegnungen ignoriert werden, selbst wenn der Stream bestellt wird. Abgesehen davon kommt das Wort "kommutativ" in den Stream-Javadocs nicht vor, daher sagt uns seine Abwesenheit nicht viel.
Aleksandr Dubinsky
2
@AleksandrDubinsky: Genau, in der Dokumentation wird Kommutativ nicht erwähnt , da es für die Reduktionsoperation nicht relevant ist . Der wichtige Teil ist: "[..] Eine ordnungsgemäß konstruierte Reduktionsoperation ist von Natur aus parallelisierbar, solange die zur Verarbeitung der Elemente verwendeten Funktionen assoziativ sind [..]."
Nosid
2
@Aleksandr Dubinsky: Natürlich ist es keine „theoretische Frage der Spezifikation“. Es macht den Unterschied, ob reduce((a,b)->b)es sich um eine korrekte Lösung handelt, um das letzte Element (natürlich eines geordneten Streams) zu erhalten, oder nicht. Die Aussage von Brian Goetz macht einen Punkt, weiter besagt die API-Dokumentation , dass reduce("", String::concat)es sich um eine ineffiziente, aber korrekte Lösung für die Verkettung von Zeichenfolgen handelt, die die Aufrechterhaltung der Begegnungsreihenfolge impliziert. Die Absicht ist bekannt, die Dokumentation muss aufholen.
Holger
42

Wenn Sie eine Sammlung (oder allgemeiner eine Iterable) haben, können Sie die von Google Guava verwenden

Iterables.getLast(myIterable)

als praktischer Oneliner.

Peti
quelle
1
Und Sie können einen Stream einfach in einen iterierbaren konvertieren:Iterables.getLast(() -> data.careas.stream().filter(c -> c.bbox.orientationHorizontal).iterator())
shmosel
10

Ein Liner (kein Stream erforderlich;):

Object lastElement = list.get(list.size()-1);
nimo23
quelle
30
Wenn die Liste leer ist, wird dieser Code ausgelöst ArrayIndexOutOfBoundsException.
Dragon
8

Guave hat eine spezielle Methode für diesen Fall:

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

Es ist gleichbedeutend mit, stream.reduce((a, b) -> b)aber die Entwickler behaupten, dass es eine viel bessere Leistung hat.

Aus der Dokumentation :

Die Laufzeit dieser Methode liegt zwischen O (log n) und O (n) und ist bei effizient aufteilbaren Streams besser.

Es ist erwähnenswert, dass sich diese Methode wie folgt verhält, wenn der Stream ungeordnet ist findAny().

k13i
quelle
1
@ZhekaKozlov Art ... Holgers zeigte einige Fehler mit sich hier
Eugene
0

Wenn Sie die letzten N Elemente erhalten müssen. Verschluss kann verwendet werden. Der folgende Code verwaltet eine externe Warteschlange mit fester Größe, bis der Stream das Ende erreicht.

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

Eine andere Option könnte darin bestehen, den Reduzierungsvorgang unter Verwendung der Identität als Warteschlange zu verwenden.

    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
quelle
-1

Sie können auch die Funktion skip () wie folgt verwenden ...

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

es ist super einfach zu bedienen.

Parag Vaidya
quelle
Hinweis: Sie sollten sich beim Umgang mit großen Sammlungen (Millionen von Einträgen) nicht auf das "Überspringen" des Streams verlassen, da "Überspringen" implementiert wird, indem alle Elemente durchlaufen werden, bis die N-te Zahl erreicht ist. Ich habe es versucht. War von der Performance im Vergleich zu einer einfachen Get-by-Index-Operation sehr enttäuscht.
java.is.for.desktop
1
auch wenn die Liste leer ist, wird sie werfenArrayIndexOutOfBoundsException
Jindra Vysocký