fbpx

Streams en JAVA 8

Un stream es una secuencia de objetos que admite varios métodos que se pueden canalizar para producir el resultado deseado.
Los streams de Java 8 no deben confundirse con los flujos de E/S de Java (por ejemplo, FileInputStream, etc.) estos tienen muy poco que ver entre sí.

Los streams son envoltorios (wrappers) alrededor de una fuente de datos, lo que nos permite operar con esa fuente de datos y hacer que el procesamiento masivo sea conveniente y rápido.

Vídeo explicativo

Creación de streams

Hay muchas formas de crear una instancia de stream en JAVA 8.

Empty Stream

Podemos usar el método empty() en el caso de la creación de una secuencia vacía:

Stream<String> streamEmpty = Stream.empty();

Stream de una Colección (Collection)

También podemos crear un flujo de cualquier tipo de Colección (Collection, List, Set):

List<String> lista = Arrays.asList("a", "b", "c");
Stream<String> streamDeLista = lista.stream();

Stream de Array

También podemos crear un stream a partir de un array existente o de parte de una matriz:

String[] arr = new String[]{"a", "b", "c"};
Stream<String> streamDeArrayFull = Arrays.stream(arr);

Stream.builder()

Cuando se usa el constructor, el tipo deseado debe especificarse adicionalmente en la parte derecha de la instrucción; de lo contrario, el método build() creará una instancia de Stream

Stream streamBuilder = Stream.builder().add("a").add("b").add("c").build();

Stream.generate()

El método generate() acepta un Supplier<T> para la generación de elementos. Como el flujo resultante es infinito, el desarrollador debe especificar el tamaño deseado, o el método generate() funcionará hasta que alcance el límite de memoria:

Stream<String> streamGenerated = Stream.generate(() -> "valor").limit(10);

Stream.iterate()

Otra forma de crear un flujo infinito es usando el método iterate():

Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(20);

Stream de Primitivos

Java 8 ofrece la posibilidad de crear streams a partir de tres tipos primitivos: int, long y double. Como Stream es una interfaz genérica y no hay forma de usar primitivas como parámetro de tipo con genéricos, se crearon tres nuevas interfaces especiales: IntStream, LongStream, DoubleStream.

IntStream intStream = IntStream.range(1, 3);
LongStream longStream = LongStream.rangeClosed(1, 3);

Stream de String

También podemos usar String como fuente para crear un flujo con la ayuda del método chars() de la clase String. Dado que no hay una interfaz para CharStream en JDK, en su lugar usamos IntStream para representar un flujo de caracteres.

IntStream streamDeChars = "abc".chars();

Stream de File

Además, la clase Files de Java NIO nos permite generar un Stream de un archivo de texto a través del método lines(). Cada línea del texto se convierte en un elemento de la transmisión:

Path path = Paths.get("C:\\fichero.txt");
Stream<String> streamDeStrings = Files.lines(path);
Stream<String> streamConCharset = Files.lines(path, Charset.forName("UTF-8"));

Stream Operaciones

Se dividen en operaciones intermedias (retornar Stream) y operaciones terminales (retornar un resultado de tipo definido). Las operaciones intermedias permiten el encadenamiento.

También vale la pena señalar que las operaciones en las secuencias no cambian la fuente.

He aquí un ejemplo rápido:

long contador = list.stream().distinct().count();

Entonces, el método distinct() representa una operación intermedia, que crea una nueva secuencia de elementos únicos de la secuencia anterior. Y el método count() es una operación de terminal, que devuelve el tamaño del flujo.

Iterando

La API Stream ayuda a sustituir los bucles for, for-each y while. Permite concentrarse en la lógica de operación, pero no en la iteración sobre la secuencia de elementos. Por ejemplo:

for (String string : lista) {
    if (string.contains("a")) {
        return true;
    }
}

Este código se puede cambiar solo con una línea de código Java 8:

boolean algunoContieneAMinuscula = list.stream().anyMatch(valor -> valor.contains("a"));

Filter

El método filter() nos permite seleccionar un flujo de elementos que satisfacen un predicado.

Por ejemplo, considere la siguiente lista:

ArrayList<String> lista = new ArrayList<>();
lista.add("Antonio");
lista.add("Maria");
lista.add("Juan");
lista.add("Pedro");
lista.add("Rafael");
lista.add("Salvador");

El siguiente código crea un Stream de List, encuentra todos los elementos de este flujo que contienen el carácter “a” y crea un stream que contiene solo los elementos filtrados:

Stream stream = list.stream().filter(element -> element.contains("a"));

Map


Para convertir elementos de un Stream aplicándoles una función especial y recopilar estos nuevos elementos en un Stream, podemos usar el método map():

List uris = new ArrayList<>();
uris.add("C:\fichero.txt");
Stream stream = uris.stream().map(uri -> Paths.get(uri));

Entonces, el código anterior convierte Stream en Stream al aplicar una expresión lambda específica a cada elemento de la secuencia inicial.

Si tiene una secuencia en la que cada elemento contiene su propio stream y desea crear una secuencia de estos elementos internos, debe usar el método flatMap():

List<Detalle> detalles = new ArrayList<>();
detalles.add(nuevo Detalle());
Stream<Detalle> corriente = detalles.stream().flatMap(detalle -> detalle.getPartes().stream());

En este ejemplo, tenemos una lista de elementos de tipo Detalle. La clase Detalle contiene un campo PARTES, que es List. Con la ayuda del método flatMap(), cada elemento del campo PARTES se extraerá y agregará a la nueva secuencia resultante. Después de eso, se perderá el Stream inicial.

Match

La API Stream ofrece un práctico conjunto de instrumentos para validar elementos de una secuencia de acuerdo con algún predicado. Para hacer esto, se puede usar uno de los siguientes métodos: anyMatch(), allMatch(), noneMatch(). Sus nombres se explican por sí mismos. Esas son operaciones de terminal que devuelven un valor booleano:

boolean alMenosUnoContieneHMinuscula = list.stream().anyMatch(element -> element.contains("h")); // verdadero
boolean todosContienenHMinuscula = list.stream().allMatch(element -> element.contains("h")); // falso
boolean ningunoContieneHMinuscula  = list.stream().noneMatch(element -> element.contains("h")); // falso

Para flujos vacíos, el método allMatch() con cualquier predicado dado devolverá verdadero:

Stream.empty().allMatch(Objetos::nonNull); // verdadero

Este es un valor predeterminado sensato, ya que no podemos encontrar ningún elemento que no satisfaga el predicado.

De manera similar, el método anyMatch() siempre devuelve falso para secuencias vacías:

Stream.empty().anyMatch(Objects::nonNull); // falso

Nuevamente, esto es razonable, ya que no podemos encontrar un elemento que satisfaga esta condición.

Reduce

La API Stream permite reducir una secuencia de elementos a algún valor de acuerdo con una función específica con la ayuda del método reduce() del tipo Stream. Este método toma dos parámetros: primero, valor de inicio, segundo, una función de acumulador.

Imagina que tienes un List y quieres tener una suma de todos estos elementos y algún Integer inicial (en este ejemplo 23). Entonces, puede ejecutar el siguiente código y el resultado será 26 (23 + 1 + 1 + 1).

List<Integer> integers = Arrays.asList(1, 1, 1);
Integer reducido = integers.stream().reduce(23, (a, b) -> a + b);

Collect

La reducción también puede proporcionarse mediante el método collect() de tipo Stream. Esta operación es muy útil en el caso de convertir un stream en una colección o un mapa y representar un stream en forma de una sola cadena. Hay una clase de utilidad Collectors que proporciona una solución para casi todas las operaciones de recolección típicas. Para algunas tareas no triviales, se puede crear un recopilador personalizado.

Lista lista de resultados<br>= list.stream().map(element -> element.toUpperCase()).collect(Collectors.toList());

Este código usa la operación de terminal collect() para reducir un Stream a List.

Stream llamada secuencial

Podemos instanciar un Stream y tener una referencia accesible a ella, siempre que solo se llamen operaciones intermedias. Ejecutar una operación terminal hace que una secuencia sea inaccesible.

Si necesitamos más de una modificación, podemos encadenar operaciones intermedias. Supongamos que necesitamos sustituir cada elemento del Stream con una subcadena de los primeros caracteres. Podemos hacer esto encadenando los métodos skip() y map():

Stream stream = stream.skip(1).map(element -> element.substring(0, 3));

Invocación Perezosa (Lazy)

Las operaciones intermedias son perezosas (lazy). Esto significa que se invocarán solo si es necesario para la ejecución de la operación del terminal.

List<String> lista = Arrays.asList(“abc1”, “abc2”, “abc3”);
Stream<String> stream = lista.stream().filter(valor-> {
System.out.println("Procesando elemento: " + valor);
return valor.contains("2");
});

Como tenemos una fuente de tres elementos, podemos suponer que el método filter() se llamará tres veces y se imprimira por pantalla “Procesando elemento: ” 3 veces. Sin embargo, ejecutar este código no imprime nada por pantalla, por lo que el método filter() ni siquiera se llamó una vez. La razón por la que falta la operación de la terminal.

Si en vez de eso llamamos al final a una operacion terminal:

List<String> lista = Arrays.asList(“abc1”, “abc2”, “abc3”);
long contador = lista.stream().filter(valor-> {
System.out.println("Procesando elemento: " + valor);
return valor.contains("2");
}).count();

El resultado muestra que llamamos al método filter() 3 veces y al método count() una vez. Esto se debe a que la canalización se ejecuta verticalmente.