Saltar al contenido principal

Métodos de Stream

En esta sección introduciremos los métodos de Stream más utilizados.

Es muy importante tener en cuenta que los Stream no son reutilizables, cada operación intermedia sobre un Stream devuelve un Stream transformado, pero el original se pierde.

Para todos los ejemplos, supongamos la siguiente clase Cliente:

public class Cliente {
private String dni;
private String nombre;
private int edad;

public Cliente(String dni, String nombre, int edad) {
this.dni = dni;
this.nombre = nombre;
this.edad = edad;
}

public String getDni() {
return dni;
}

public String getNombre() {
return nombre;
}

public int getEdad() {
return edad;
}
}

Método intermedio de filtrado

El método para filtrar elementos de un stream es filter:

Stream<T> filter(Predicate<T> pred);

Al método filter se le pasa un predicado que filtrará los elementos, y que solo dejará en el nuevo Stream aquellos elementos que satisfagan el predicado. Este método es un método intermedio, ya que devuelve un nuevo Stream, del cual se podrán seguir realizando transformaciones.

ArrayList<Cliente> clientes = new ArrayList<>();
clientes.add("111", "Manu", 22);
clientes.add("222", "Manuel", 32);
clientes.add("333", "Laura", 42);
clientes.add("444", "Antía", 52);

// Filtrar todos los clientes que tienen más de 30 años.
clientes.stream()
.filter(c -> c.getEdad() > 30)

Método intermedio de ordenamiento

Vamos a ver un ejemplo de cómo podemos ordenar elementos de un Stream.

Para ordenar streams tenemos el método sorted:

Stream<T> sorted()
Stream<T> sorted(Comparator<T> comparator)

Que devuelve un nuevo Stream con los elementos ordenados según su orden natural:

clientes.stream()
.sorted()

En este caso el orden natural de la clase Cliente es el ordenamiento por DNI.

El método sorted() está sobrecargado y puede admitir como parámetro un comparador con el criterio de ordenación de los elementos. Por ejemplo, si queremos que los elementos se comparen por el nombre:

clientes.stream
.sorted((x,y) -> x.getNombre().compareTo(y.getNombre()))

Método intermedio de mapeo

El método map() recibe una función que nos permite transformar los elementos del Stream original del tipo T y devuelve un Stream con los elementos transformados del tipo R.

Stream<R> map(Function<T, R> mapper);

Por ejemplo, queremos realizar una función que a partir de la clase Cliente nos devuelva solo los DNI de los clientes que son de la clase String.

clientes.stream().
map(c -> c.getDni())

Método intermedio distinct()

El método intermedio distinct() elimina los elementos repetidos del streams.

Stream<T> distinct()

En este ejemplo vamos a eliminar los clientes repetidos.

clientes.stream().distinct();

Para poder utilizar el método distinct() en objetos de clases creadas por nosotros, debemos redefinir el método Object equals() y el método int hashCode().

Por ejemplo, si queremos indicar que dos clientes son iguales cuando tienen el mismo DNI, el método int hashCode() se implementaría del siguiente modo utilizando el método estático static int hash(Object... values) de la clase Objects:

@Override
public int hashCode() {
return Objects.hash(this.dni);
}

Métodos intermedios limit() y skip()

Los métodos intermedios skip() y limit() sirven para manipular la cantidad de elementos que procesamos de un flujo de datos:

  • skip(n): Omite los primeros n elementos del Stream.
  • limit(n): Toma solo los primeros n elementos del Stream.
List<String> nombres = List.of("Ana", "Bruno", "Carlos", "Diana", "Elena");

nombres.stream()
.skip(2) // Omite los 2 primeros ("Ana" y "Bruno")

List<Integer> numeros = List.of(1, 2, 3);
numeros.stream()
.skip(1) // Omite el primer elemento

skip() y limit() se pueden usar juntos para seleccionar un rango específico de elementos.

List<String> nombres = List.of("Ana", "Bruno", "Carlos", "Diana", "Elena");

// Seleccionar los elementos del 2º al 4º
nombres.stream()
.skip(1) // Omitir "Ana"
.limit(3) // Coge solo "Bruno", "Carlos" y "Diana"

Método terminal foreach()

El método forEach() se usa para aplicar una acción a cada elemento del flujo de datos. Es muy útil para imprimir elementos o realizar operaciones sin modificar los datos.

void forEach(Consumer<T> action)

Ejemplo para imprimir los nombres de los clientes:

clientes.stream().forEach(c -> System.out.println(c.getNombre()));

Método terminal collect()

El método collect() nos permite agrupar elementos de un Stream en una colección, un mapa, una cadena o realizar estadísticas de los datos. Vamos a ver algunos ejemplos sencillos.

En todos los casos se le pasa un objeto Collector que se obtiene a partir de distintos métodos de la clase Collectors.

Veamos algunos de los métodos más interesantes de Collectors:

toList()

Obtenemos una lista a partir de un stream.

public static <T> Collector<T,?,List<T>> toList()

Si queremos una lista con valores de un Stream de números enteros le pasamos como argumento el objeto de Collector devuelto por Collectors.toList():

List<Integer> listaImpares = Stream.of(2, 5, 1, 4, -6 , -3 , -3)
.collect(Collectors.toList());

Collectors.toCollection()

Podemos también obtener una implementación concreta de una colección.

public static <T,C extends Collection<T>> Collector<T,?,C> toCollection(Supplier<C> collectionFactory)

Podemos obtener también una implementación concreta de una lista, por ejemplo de ArrayList:

List<Integer> listaImpares = Stream.of(2,5,1,4,-6,-3,-3)
.collect(Collectors.toCollection(ArrayList::new));

Con ArrayList::new se está indicando que se va a llamar al constructor de la clase ArrayList.

toMap()

Si queremos obtener un mapa debemos utilizar Collectors.toMap() y deberemos especificar qué atributo es clave y qué atributo es valor:

public static <T,K,U> Collector<T,?,Map<K,U>> toMap(Function<T,K> keyMapper, Function<T,U> valueMapper)
Map<String,String> mapaClientes = Clientes.stream()
.collect(Collectors.toMap(c -> c.getDni(), c -> c.getNombre()));

joining()

Concatenar String en una única cadena.

public static Collector<CharSequence,?,String> joining() // Une todo en una única cadena
public static Collector<CharSequence,?,String> joining(CharSequence delimiter) // Une todo con un separador

Ejemplo: une una lista de String en una sola cadena separada por una coma.

Stream.of("Java", "Python", "C++")
.collect(Collectors.joining(", "));

groupingBy()

Agrupar elementos en un Map. Agrupa elementos según una clave.

public static <T,K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<T, K> classifier)

Ejemplo:

Map<Integer, List<String>> agrupadoPorLongitud = List.of("gato", "perro", "casa", "auto", "árbol").stream()
.collect(Collectors.groupingBy(s -> s.length()));

Método terminal count()

El método terminal count() devuelve el número de elementos del stream.

long count()

En este ejemplo vamos a ver el número de clientes

clientes.stream().count();

Métodos terminales max() y min()

Los métodos terminales max() y min() en Stream<T> se usan para encontrar el máximo o mínimo elemento de un flujo según un criterio definido con un Comparator. Ambos devuelven un Optional<T> porque el Stream puede estar vacío.

Optional<T> max(Comparator<T> comparator)
Optional<T> min(Comparator<T> comparator)
List<Producto> productos = Arrays.asList(
new Producto("Móvil", 599.99),
new Producto("Portátil", 1299.99),
new Producto("Tablet", 399.99)
);

Optional<Producto> masCaro = productos.stream()
.max(Comparator.comparing(producto -> producto.getPrecio));

Método terminal reduce()

El método reduce() en streams de Java es una operación terminal que permite reducir los elementos de un stream a un único valor. Se utiliza para realizar operaciones de agregación, como la suma, la multiplicación o la concatenación, de manera eficiente y funcional.

Java proporciona dos variantes del método reduce():

  • Con acumulador y sin valor inicial.
  • Con acumulador y con valor inicial.

Con acumulador y sin valor inicial

Usa una función binaria para combinar elementos del flujo. Retorna un Optional<T> ya que el flujo puede estar vacío.

Ejemplo: encontrar el producto de todos los números de un flujo.

List<Integer> numeros = Arrays.asList(2, 3, 4, 5);
Optional<Integer> resultado = numeros.stream().reduce((a, b) -> a * b);
System.out.println(resultado.get()); // 120

La operación multiplica cada par de elementos progresivamente:

  • Paso 1 - Multiplica los dos primeros números: (2 * 3) = 6
  • Paso 2 - Multiplica el resultado por el siguiente número: (6 * 4) = 24
  • Paso 3 - Multiplica el resultado por el último número: (24 * 5) = 120
  • El resultado final será 120.

Con acumulador y con valor inicial

Usa un valor de identidad, que actúa como base de la operación. Siempre devuelve un resultado (nunca Optional).

T reduce(T identidad, BinaryOperator<T> acumulador)

Ejemplo: sumar una lista de números

List<Integer> numeros = Arrays.asList(2, 3, 4, 5);
int suma = numeros.stream().reduce(0, (a, b) -> a + b);
System.out.println(suma); // 14

El método reduce() recorre la lista y aplica la suma de forma progresiva:

  • Paso 1 - El valor inicial es 0, entonces sumamos con el primer elemento: 0 + 2 = 2
  • Paso 2 - Sumamos el siguiente elemento: 2 + 3 = 5
  • Paso 3 - Sumamos el siguiente elemento: 5 + 4 = 9
  • Paso 4 - Sumamos el último elemento: 9 + 5 = 14
  • Resultado final: 14.