Stream
La clase Stream<T> es un procesador de secuencias de elementos que aplica operaciones en modo funcional.
Es importante tener en cuenta que los streams son de uso único, e intentar reutilizarlos provocará lanzamiento de excepciones o efectos secundarios silenciosos; esto se debe a que los streams no han sido diseñados para guardar elementos.
Los streams se evalúan de manera vaga, lo que quiere decir que las operaciones definidas en un Stream no se ejecutan hasta que se llame a una operación terminal.
Crear objetos Stream
Stream.empty()para streams vacíos.Stream.of(T value)para un stream de un solo objeto.Stream.of(T... values)para arrays y varargs.collection.stream()para objetos que implementenCollection<T>por ejemplo objetosListySet.StreamSupport.stream(iterable.spliterator(), false)para objetos que implementenIterable<T>.Stream.concat(Stream<? extends T> a, Stream<? extends T> b))para combinar dos streams en uno.- Si queremos combinar varios streams, una alternativa más usable sería:
Stream.of(stream1, stream2, stream3, stream4).flatMap(i -> i) - Si queremos generar un stream a partir de elementos individuales, una alternativa más usable sería:
Stream.<String>builder().add("a").add("b").build()
- Si queremos combinar varios streams, una alternativa más usable sería:
Stream.generate(Supplier<T> s)yStream.iterate(T seed, UnaryOperator<T> f)para streams infinitos.
Streams concurrentes
Permitir habilitar la ejecución en paralelo de Stream basta con llamar al método parallelStream().
Operaciones intermedias
distinct(): elimina elementos iguales según los métodosequalsyhashCode(aunque la documentación no indique el uso del segundo).filter(Predicate<? super T> predicate): elimina elementos para los que el predicado retornafalse.skip(long n): elimina los primeros n elementos del stream.limit(long maxSize): limita el stream al número indicado de elementos.sorted(),sorted(Comparator<? super T> comparator): ordenan los elementos de acuerdo a su ordenación natural o al comparador.map(Function<? super T,? extends R> mapper): transforma todos los elementos del stream aplicando la función.flatMap(Function<? super T,? extends Stream<? extends R>> mapper)es una especialización para “aplanar” streams.// Si tuvieramos un stream de listas que queremos convertir a uno de elementos Stream<String> stringStream = stringListStream.flatMap(Collection::stream)
peek(Consumer<? super T> action): ejecuta la acción para cada elemento del stream sin modificarlo.
Operaciones terminales
Estos métodos retornan objetos que no son Stream; para ello, se ejecutan todas las operaciones definidas en este.
allMatch/anyMatch/noneMatch(Predicate<? super T> predicate): indica si el predicado retornatruepara todos, alguno o ninguno de sus miembros.count(): número de elementos.forEach/forEachOrdered(Consumer<? super T> action): ejecuta la acción para cada elemento del stream, en orden si este aplica; igual que el operador intermediopeek, pero cerrando el stream.max/min(Comparator<? super T> comparator): retorna el máximo/mínimo del stream según el comparador.toArray(),toArray(IntFunction<A[]> generator): guarda los elementos en un arrayObject[]o del tipo dado por el generador.reduce: agrupa los elementos sobre sí mismos.collect(Collector<? super T, A, R> collector): agrupa los elementos sobre una estructura de datos.
Reduce
reduce agrupa los elementos del stream combinándolos sobre sí mismos.
Los elementos a tener en cuenta en una reducción son:
- Identidad – valor inicial de la reducción y, por este motivo, valor retornado si no hay resultado.
- Acumulador – función que combina la reducción parcial con el siguiente elemento y la retorna.
- Combinador – función que combina dos reducciones parciales y retorna la combinación; sólo es necesario si se está reduciendo en paralelo o si la clase a reducir es distinta a la de la reducción (por ejemplo, si reducimos un
Stream<Examen>por su nota de tipoDouble).
Las implementaciones de reduce son
Optional<T> reduce(BinaryOperator<T> accumulator): retorna el valor de la acumulación a partir del primer elemento, siempre que el stream no esté vacío.T reduce(T identity, BinaryOperator<T> accumulator): retorna el valor de la acumulación a partir de la identidad.-
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner): permite usar cualquier objeto como identidad, teniendo que definir un acumulador y combinador válidos.Aunque parecido a
collect(supplier, accumulator, combiner), enreducela referencia del agrupado se actualiza al valor retornado por el acumulador, lo que nos permite utilizar tipos sencillos de manera directa
Collect
collect(Collector<? super T, A, R> collector) recoge los elementos del stream en una estructura de datos.
La interfaz Collector, instanciable como clase anónima o mediante Collector.of, debe implementar los siguientes métodos:
supplier(): retorna la función que crea un acumulador vacío (de tipo A).accumulator(): retorna la función que añade un elemento al acumulador.combiner(): retorna la función que combina dos acumuladores y retorna el acumulador combinado.finisher(): retorna la función que convierte un acumulador en el tipo de resultado R.characteristics(): retorna un conjunto de flags usadas para optimizar, que debería ser unSetinmutable.CONCURRENTindica que el colector soporta concurrencia.UNORDEREDindica que el colector no garantiza el orden de los elementos; esto tiene sentido si la reducción no tiene un orden intrínseco (comoMapoSet).IDENTITY_FINISHindica que el acumulador es el resultado, por lo que un casteo entre los tiposAyRdebe ser posible.
collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner) genera un colector asumiendo que el finalizador es una función identidad i -> i que hace que el mismo acumulador sea el resultado. Este colector no usa características.
collect actualiza el estado del acumulador en lugar de sustituirlo. dado que Java siempre hace paso de variables por valor, esto significa que no podemos trabajar con tipos simples como Integer o String; para ello tendremos que utilizar reduce o envolver nuestro tipo de alguna manera (por ejemplo, usando AtomicInteger o StringBuilder).
Collectors
Para hacer más fácil el trabajo de generar colectores, la clase Collectors implementa varios métodos que simplifican el proceso.
Para agrupar en un String, joining une strings (u otros CharSequence); podemos pasarle separador entre elementos, prefijo y sufijo.
Para agrupar en estructuras de datos:
toList,toSetytoCollectionpara meterlos en colecciones.toMapytoConcurrentMappara generar un mapa a partir de los elementos; si varios tienen la misma clave, el agrupado lanzará una excepción si no ha recibido una función de combinación de valores.groupingByygroupingByConcurrentpara generar un mapa en el que cada clave tiene una lista con los valores asociados.partitioningBypara generar un mapa con dos listas de elementos agrupados según su resultado en el predicado.
Existen otros colectores como accesos alternativos a funciones de Stream (p.e. stream.collect(Collectors.counting()) equivale a stream.count(); aunque parece redundante y es recomenable evitar estos colectores por legibilidad, estos nos permiten encapsular lógica más comodamente si es necesario (por ejemplo, una función que reciba un colector según el que retorne el máximo, el mínimo o la media de una lista de números).
Streams de primitivos
Si queremos trabajar con primitivos, disponemos de las interfaces IntStream, LongStream y DoubleStream.
Estas clases ponen a nuestra disposición, además de las maneras ya indicadas de crear un stream,
los métodos range y rangeClosed para generar streams con rangos numéricos.
Stream también tiene métodos mapToX y flatMapToX para generarlos a partir de streams normales.
Un stream de primitivos puede convertirse en uno normal a través de los métodos boxed o mapToObj.
Ejercicios
Utilizando la API de Stream:
- Obten los enteros que sean cuadrados perfectos entre 100 y 1000.
- Para un número n, retorna un string de n caracteres hexadecimales aleatorios.
- Para una lista de strings, retorna el número total de palabras entre todos los elementos.
- Para una lista de palabras, obtén su mapa de diccionario: la clave es la primera letra, y la lista de valores son las palabras que empiezan con dicha letra. Opcionalmente:
- Haz que las letras disponibles sean todas en mayúscula.
- Haz que las letras disponibles estén ordenadas alfabéticamente.
- Haz que las palabras por letra estén ordenadas alfabéticamente.
- Haz que la ordenación alfabética sea independiente de mayúsculas y acentos.