Obtenga el último elemento de Stream / List en una sola línea

118

¿Cómo puedo obtener el último elemento de una secuencia o lista en el siguiente código?

¿Dónde 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?

Como puede ver, conseguir el primer elemento, con un cierto filter, no es difícil.

Sin embargo, obtener el último elemento en una sola línea es un verdadero dolor:

  • Parece que no puedo obtenerlo directamente de a Stream. (Solo tendría sentido para flujos finitos)
  • También parece que no puede obtener cosas como first()y last()desde la Listinterfaz, lo cual es realmente una molestia.

No veo ningún argumento para no proporcionar un método first()y last()en la Listinterfaz, ya que los elementos allí están ordenados y, además, se conoce el tamaño.

Pero según la respuesta original: ¿Cómo obtener el último elemento de un finito Stream?

Personalmente, esto es lo más cercano que pude conseguir:

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

Sin embargo, implica el uso de un indexOfen cada elemento, lo que probablemente no es lo que generalmente desea, ya que puede afectar el rendimiento.

skiwi
fuente
10
Guava proporciona lo Iterables.getLastque lleva Iterable pero está optimizado para trabajar List. Una manía es que no tiene getFirst. La StreamAPI en general es horriblemente anal, omitiendo muchos métodos de conveniencia. LINQ de C #, por restricción, se complace en proporcionar .Last()e incluso .Last(Func<T,Boolean> predicate), aunque también admite infinitos Enumerables.
Aleksandr Dubinsky
@AleksandrDubinsky votó a favor, pero una nota para los lectores. StreamAPI no es completamente comparable LINQya que ambas se realizan en un paradigma muy diferente. No es peor ni mejor, simplemente es diferente. Y definitivamente, algunos métodos están ausentes no porque los desarrolladores de Oracle sean incompetentes o malos :)
fasth
1
Para un verdadero one-liner, este hilo puede ser útil.
Quantum

Respuestas:

185

Es posible obtener el último elemento con el método Stream :: reduce . La siguiente lista contiene un ejemplo mínimo para el caso general:

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

Esta implementación funciona para todos los flujos ordenados (incluidos los flujos creados a partir de listas ). Para flujos desordenados , por razones obvias, no se especifica qué elemento se devolverá.

La implementación funciona tanto para flujos secuenciales como paralelos . Eso puede resultar sorprendente a primera vista y, lamentablemente, la documentación no lo indica explícitamente. Sin embargo, es una característica importante de las transmisiones y trato de aclararla:

  • El Javadoc para el método Stream :: reduce establece que " no está obligado a ejecutarse secuencialmente " .
  • El Javadoc también requiere que la "función acumuladora debe ser una función asociativa , sin interferencias y sin estado para combinar dos valores" , lo que obviamente es el caso de la expresión lambda (first, second) -> second.
  • El Javadoc para operaciones de reducción establece: "Las clases de streams tienen múltiples formas de operaciones de reducción general, llamadas reduce () y collect () [..]" y "una operación de reducción construida correctamente es inherentemente paralelizable , siempre que la función (s ) utilizados para procesar los elementos son asociativos y apátridas ".

La documentación para los coleccionistas estrechamente relacionados es aún más explícita: "Para garantizar que las ejecuciones secuenciales y paralelas produzcan resultados equivalentes , las funciones del recopilador deben satisfacer restricciones de identidad y asociatividad ".


Volviendo a la pregunta original: el siguiente código almacena una referencia al último elemento de la variable lasty genera una excepción si la secuencia está vacía. La complejidad es lineal en la longitud de la corriente.

CArea last = data.careas
                 .stream()
                 .filter(c -> c.bbox.orientationHorizontal)
                 .reduce((first, second) -> second).get();
nosid
fuente
¡Bonito, gracias! Por cierto, ¿sabe si es posible omitir un nombre (tal vez mediante el uso de un _o similar) en los casos en que no necesita un parámetro? Así sería: .reduce((_, current) -> current)si solo eso permitiera una sintaxis válida.
Skiwi
2
@skiwi puede usar cualquier nombre de variable legal, por ejemplo: .reduce(($, current) -> current)o .reduce((__, current) -> current)(doble subrayado).
Assylias
2
Técnicamente, es posible que no funcione para ninguna transmisión. La documentación a la que apunta, así como Stream.reduce(BinaryOperator<T>)tampoco menciona si reduceobedece al orden de encuentro, y una operación de terminal es libre de ignorar el orden de encuentro incluso si el flujo está ordenado. Como acotación al margen, la palabra "conmutativa" no aparece en los javadocs Stream, por lo que su ausencia no nos dice mucho.
Aleksandr Dubinsky
2
@AleksandrDubinsky: Exactamente, la documentación no menciona conmutativa , porque no es relevante para la operación de reducción . La parte importante es: "[..] Una operación de reducción construida correctamente es intrínsecamente paralelizable, siempre que las funciones utilizadas para procesar los elementos sean asociativas [..]".
nosid
2
@Aleksandr Dubinsky: por supuesto, no es una "cuestión teórica de la especificación". Marca la diferencia entre reduce((a,b)->b)ser una solución correcta para obtener el último elemento (de un flujo ordenado, por supuesto) o no. La declaración de Brian Goetz hace un punto, además la documentación de la API indica que reduce("", String::concat)es una solución ineficiente pero correcta para la concatenación de cadenas, lo que implica el mantenimiento del orden de encuentro. La intención es bien conocida, la documentación debe ponerse al día.
Holger
42

Si tienes una colección (o más general un iterable) puedes usar Google Guava's

Iterables.getLast(myIterable)

como un delineador a mano.

Peti
fuente
1
Y puede convertir fácilmente una transmisión en iterable:Iterables.getLast(() -> data.careas.stream().filter(c -> c.bbox.orientationHorizontal).iterator())
shmosel
10

Un trazador de líneas (sin necesidad de flujo;):

Object lastElement = list.get(list.size()-1);
nimo23
fuente
30
Si la lista está vacía, este código arrojará ArrayIndexOutOfBoundsException.
Dragón
8

Guava tiene un método dedicado para este caso:

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

Es equivalente a stream.reduce((a, b) -> b) pero los creadores afirman que tiene un rendimiento mucho mejor.

De la documentación :

El tiempo de ejecución de este método estará entre O (log n) y O (n), con un mejor rendimiento en flujos divisibles de manera eficiente.

Vale la pena mencionar que si la transmisión no está ordenada, este método se comporta como findAny().

k13i
fuente
1
@ZhekaKozlov algo así como ... Holger mostró algunos defectos aquí
Eugene
0

Si necesita obtener el último número N de elementos. Se puede utilizar el cierre. El siguiente código mantiene una cola externa de tamaño fijo hasta que la secuencia llega al final.

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

Otra opción podría ser utilizar la operación de reducción utilizando la identidad como cola.

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

También puede usar la función skip () como se muestra a continuación ...

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

es superfácil de usar.

Parag Vaidya
fuente
Nota: no debe confiar en el "salto" de la transmisión cuando se trata de colecciones enormes (millones de entradas), porque el "salto" se implementa iterando a través de todos los elementos hasta que se alcanza el número enésimo. Lo intenté. Me decepcionó mucho el rendimiento, en comparación con una simple operación de obtención de índices.
java.is.for.desktop
1
también si la lista está vacía, arrojaráArrayIndexOutOfBoundsException
Jindra Vysocký