Obtenez le dernier élément de Stream / List en une seule ligne


118

Comment puis-je obtenir le dernier élément d'un flux ou d'une liste dans le code suivant?

data.careasest un 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?

Comme vous pouvez le voir, obtenir le premier élément, avec un certain filter, n'est pas difficile.

Cependant, obtenir le dernier élément d'un one-liner est une vraie douleur:

  • Il semble que je ne puisse pas l'obtenir directement à partir d'un fichier Stream. (Cela n'aurait de sens que pour les flux finis)
  • Il semble également que vous ne pouvez pas obtenir des choses comme first()et à last()partir de l' Listinterface, ce qui est vraiment pénible.

Je ne vois aucun argument pour ne pas fournir une méthode first()and last()dans l' Listinterface, car les éléments y sont ordonnés et de plus la taille est connue.

Mais selon la réponse originale: comment obtenir le dernier élément d'un fini Stream?

Personnellement, c'est le plus proche que je puisse obtenir:

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

Cependant, cela implique l'utilisation d'un indexOfélément sur chaque élément, ce qui n'est probablement pas souhaité car cela peut nuire aux performances.


10
Guava fournit Iterables.getLastce qui prend Iterable mais est optimisé pour fonctionner avec List. Une bête noire, c'est qu'il n'en a pas getFirst. L' StreamAPI en général est horriblement anale, omettant de nombreuses méthodes pratiques. LINQ de C #, par contraste, est heureux de fournir .Last()et même .Last(Func<T,Boolean> predicate), même s'il prend également en charge des Enumerables infinis.
Aleksandr Dubinsky

@AleksandrDubinsky a voté pour, mais une note pour les lecteurs. StreamL'API n'est pas entièrement comparable LINQcar les deux sont réalisés dans un paradigme très différent. Ce n'est ni pire ni meilleur, c'est juste différent. Et certainement certaines méthodes sont absentes non pas parce que les développeurs oracle sont incompétents ou méchants :)
fasth

1
Pour un vrai one-liner, ce fil peut être utile.
quantum

Réponses:


185

Il est possible d'obtenir le dernier élément avec la méthode Stream :: reduction . La liste suivante contient un exemple minimal pour le cas général:

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

Cette implémentation fonctionne pour tous les flux ordonnés (y compris les flux créés à partir de listes ). Pour non ordonné flux , c'est pour des raisons évidentes non spécifiées quel élément sera retourné.

L'implémentation fonctionne pour les flux séquentiels et parallèles . Cela peut être surprenant à première vue, et malheureusement la documentation ne l'indique pas explicitement. Cependant, c'est une caractéristique importante des flux, et j'essaye de la clarifier:

  • Le Javadoc de la méthode Stream :: reduction indique qu'il "n'est pas contraint de s'exécuter séquentiellement " .
  • Le Javadoc exige également que la "fonction d'accumulateur soit une fonction associative , non interférente et sans état pour combiner deux valeurs" , ce qui est évidemment le cas pour l'expression lambda(first, second) -> second .
  • Le Javadoc pour les opérations de réduction déclare: "Les classes de flux ont plusieurs formes d'opérations de réduction générales, appelées réduire () et collect () [..]" et "une opération de réduction correctement construite est intrinsèquement parallélisable , tant que la fonction (s ) utilisés pour traiter les éléments sont associatifs et sans état . "

La documentation des Collecteurs étroitement liés est encore plus explicite: "Pour s'assurer que les exécutions séquentielles et parallèles produisent des résultats équivalents , les fonctions de collecteur doivent satisfaire une contrainte d' identité et d' associativité ."


Retour à la question d'origine: Le code suivant stocke une référence au dernier élément de la variable lastet lève une exception si le flux est vide. La complexité est linéaire dans la longueur du flux.

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

Gentil, merci! Savez-vous d'ailleurs s'il est possible d'omettre un nom (peut-être en utilisant un _ou similaire) dans les cas où vous n'avez pas besoin d'un paramètre? Ce serait le cas: .reduce((_, current) -> current)si seulement cette syntaxe était valide.
skiwi

2
@skiwi vous pouvez utiliser n'importe quel nom de variable légal, par exemple: .reduce(($, current) -> current)ou .reduce((__, current) -> current)(double tiret bas).
assylias

2
Techniquement, cela peut ne pas fonctionner pour aucun flux. La documentation sur laquelle vous pointez, ainsi que pour Stream.reduce(BinaryOperator<T>)ne fait aucune mention si reduceobéit à l'ordre de rencontre, et une opération de terminal est libre d'ignorer l'ordre de rencontre même si le flux est commandé. En passant, le mot «commutatif» n'apparaît pas dans les javadocs Stream, donc son absence ne nous en dit pas beaucoup.
Aleksandr Dubinsky

2
@AleksandrDubinsky: Exactement, la documentation ne mentionne pas commutative , car elle n'est pas pertinente pour l' opération de réduction . La partie importante est: "[..] Une opération de réduction correctement construite est intrinsèquement parallélisable, tant que la ou les fonctions utilisées pour traiter les éléments sont associatives [..]."
nosid

2
@Aleksandr Dubinsky: bien sûr, ce n'est pas une «question théorique de spécification». Cela fait la différence entre reduce((a,b)->b)être une solution correcte pour obtenir le dernier élément (d'un flux ordonné, bien sûr) ou non. La déclaration de Brian Goetz fait un point, en outre la documentation de l' API indique que reduce("", String::concat)c'est une solution inefficace mais correcte pour la concaténation de chaînes, ce qui implique le maintien de l'ordre des rencontres. L'intention est bien connue, la documentation doit rattraper son retard.
Holger

42

Si vous avez une collection (ou plus généralement un Iterable), vous pouvez utiliser Google Guava

Iterables.getLast(myIterable)

comme oneliner pratique.


1
Et vous pouvez facilement convertir un flux en un itérable:Iterables.getLast(() -> data.careas.stream().filter(c -> c.bbox.orientationHorizontal).iterator())
shmosel

10

Une doublure (pas besoin de flux;):

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

30
Si la liste est vide, ce code lancera ArrayIndexOutOfBoundsException.
Dragon

8

Guava a une méthode dédiée pour ce cas:

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

C'est équivalent à stream.reduce((a, b) -> b) mais les créateurs affirment que ses performances sont bien meilleures.

De la documentation :

Le runtime de cette méthode sera entre O (log n) et O (n), fonctionnant mieux sur les flux séparables efficacement.

Il convient de mentionner que si le flux n'est pas ordonné, cette méthode se comporte comme findAny().


1
@ZhekaKozlov en quelque sorte ... Holger a montré quelques défauts ici
Eugene

0

Si vous avez besoin d'obtenir le dernier nombre d'éléments N. La fermeture peut être utilisée. Le code ci-dessous maintient une file d'attente externe de taille fixe jusqu'à ce que le flux atteigne la fin.

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

Une autre option pourrait être d'utiliser une opération de réduction en utilisant l'identité comme file d'attente.

    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

Vous pouvez également utiliser la fonction skip () comme ci-dessous ...

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

c'est super simple à utiliser.


Remarque: vous ne devriez pas vous fier au "skip" du flux lorsque vous traitez d'énormes collections (des millions d'entrées), car "skip" est implémenté en itérant à travers tous les éléments jusqu'à ce que le Nième nombre soit atteint. Essayé. A été très déçu par les performances, par rapport à une simple opération get-by-index.
java.is.for.desktop

1
aussi si la liste est vide, elle lanceraArrayIndexOutOfBoundsException
Jindra Vysocký
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.