Ir al contenido principal

Streams en Java

Los streams de Java se introdujeron en Java 8 como parte de la API de Streams 🔗, proporcionando una forma moderna y funcional de procesar colecciones de datos. Los Streams permiten realizar operaciones de manera declarativa y paralela, mejorando la legibilidad y el rendimiento del código. 

Hoy en día prácticamente su uso se ha convertido en un must para cualquier profesional que se dedique a la programación.

Algunas ventajas de utilizar Java Streams 👀💥:

Código más conciso y legible

Las operaciones con Streams son más compactas y fáciles de leer.

Paralelismo

Los Streams permiten ejecutar operaciones en paralelo de manera sencilla.

Inmutabilidad

Los Streams no modifican las colecciones originales, lo que ayuda a evitar errores.

Composición de operaciones

Los Streams permiten encadenar múltiples operaciones de manera fluida.

Mejor rendimiento

Las operaciones con Streams pueden ser más eficientes gracias a optimizaciones internas.



Tipos de operaciones sobre streams


Comencemos por lo super básico... el inicio del source
	    
    List<Employee> employees = List.of(
        new Employee("Alice", 30, 70000, "Company A", List.of("Java", "Spring", "SQL")),
        new Employee("Bob", 25, 60000, "Company B", List.of("JavaScript", "React", "Node.js")),
        new Employee("Charlie", 35, 50000, "Company C", List.of("Java", "Spring", "SQL"))
    );




Source 🔥

En el ejemplo de arriba el source o fuente de datos será la lista con los tres empleados.

Operaciones intermedias 🔌

Son con las que vamos realizando las transformaciones a nuestro stream original.

map

Con map (operación intermedia) transformamos un objeto Employee a un String, en este caso el nombre del empleado en mayúsculas.

			    //Map
    List<String> employeeNames =
    employees.stream()
        .map((s) -> s.name().toUpperCase())
        .collect(Collectors.toList());
    System.out.println(employeeNames);
		

Output:
ALICE
BOB
CHARLIE


flatMap

En particular yo la utilizo frecuentemente y nos sirve para aplanar un stream en el que los elementos son colecciones de otros elementos o bien tienen alguna propiedad de tipo colección. Os muestro un ejemplo en el que devolvemos los skills únicos de los empleados en un Set:

				        
            // flatMap
    Set<String> uniqueSkills = employees.stream()
        .flatMap(employee -> employee.skills().stream())
        .collect(Collectors.toSet());
    System.out.println(uniqueSkills);

        
				
			

Output:
[Java, JavaScript, Node.js, Spring, React, SQL]

filter

Es una operación intermedia por la que podemos filtrar elementos del stream. En este caso filtramos los empleados mayores a 30 años antes de realizar el map.

		
    // Filter
    List<String> employeeFiltered = employees.stream()
        .filter((e) -> e.age()> 30)
        .map (Employee::name)
        .collect(Collectors.toList());
        System.out.println(employeeFiltered);
        
	
Output:

[Charlie]

sort

Es una operación intermedia por la que podemos ordenar los elementos del stream, de menor a mayor por edad.

			
            // Sort
    List>Employee> sortedEmployees = employees.stream()
        .sorted(Comparator.comparingInt(Employee::age))
        .collect(Collectors.toList());
    System.out.println(sortedEmployees);
    
		

Output:


[Employee[name=Bob, age=25, salary=60000.0, company=Company B], Employee[name=Alice, age=30, salary=70000.0, company=Company A], Employee[name=Charlie, age=35, salary=50000.0, company=Company C]]


Y con reversed de mayor a menor por salario:
			
      // Sort
    sortedEmployees = employees.stream()
        .sorted(Comparator.comparingDouble(Employee::salary).reversed())
        .collect(Collectors.toList());
    System.out.println(sortedEmployees);
  
		

Output:

[Employee[name=Alice, age=30, salary=70000.0, company=Company A], Employee[name=Bob, age=25, salary=60000.0, company=Company B], Employee[name=Charlie, age=35, salary=50000.0, company=Company C]]


limit


Con limit nos quedamos con los n primeros elementos del stream:

			
 	// limit
    List<Employee> limitedEmployees = employees.stream()
        .limit(2)
        .collect(Collectors.toList());
    System.out.println(skipEmployees);
				
    
    
Output:

[Employee[name=Alice, age=30, salary=70000.0, company=Company A], Employee[name=Bob, age=25, salary=60000.0, company=Company B]]

skip

Con skip ignoramos los n últimos elementos del stream:

				      // skip
    List<Employee> skipEmployees = employees.stream()
        .skip(1)
        .collect(Collectors.toList());
    System.out.println(skipEmployees);
			

Output:
[Employee[name=Bob, age=25, salary=60000.0, company=Company B], Employee[name=Charlie, age=35, salary=50000.0, company=Company C]]


Operaciones finales 📕

Para realizar la operación final de nuestro stream original, ya sea devolver algún resultado o ejecutar una acción sobre los elementos del stream resultante.

foreach

Es una operación final, lo que hace es realizar una acción sobre cada elemento del Stream. Para ello recibe un Consumer.

			  void forEach(Consumer<? super T> action)
		
			
    
        
    	List<Employee> employees = List.of(
        new Employee("Alice", 30, 70000, "Company A"),
        new Employee("Bob", 25, 60000, "Company B"),
        new Employee("Charlie", 35, 50000, "Company C")
    );

    employees.stream()
        .map((s) -> s.name().toUpperCase())
        .forEach(System.out::println);
		

En este caso lo que hacemos es pintar por salida el nombre de los empleados en mayúscula:

Output:
[ALICE, BOB, CHARLIE]



count

Con count podemos devolver el número de elementos después de realizar nuestras transformaciones. En el ejemplo devolvermos por ejemplo los empleados mayores de edad.

				        // count
    long adulltsCount = employees.stream()
        .map(Employee::age)
        .filter(a -> a > 18)
        .count();
    System.out.println(adulltsCount);
			



Output:
3


allMatch

Nos dice si todos los elementos del stream cumplen un predicado que se pasa como parámetro. Por ejemplo, ¿todos los empleados cobran más de 60000?


				
      // allMatch
    boolean allMatch = employees.stream()
        .allMatch(e -> e.salary()> 60000);
    System.out.println("Are all the employees rich : " + allMatch);
  
			

Output:


Are all the employees rich : false

noneMatch

Nos dice si ningún elemento del stream cumplen un predicado que se pasa como parámetro. Por ejemplo, ¿ningún empleado es mayor a 65 años?
			
      // noneMatch
    boolean noneMatch = employees.stream()
        .noneMatch(e -> e.age() > 65);
    System.out.println("Are all the employees below 65 : " + noneMatch);
  
		

Output:


Are all the employees below 65 : true


min


Nos devuelve el minimo elemento del stream en base a un criterio de comparación. En este caso le pesamos el criterio de edad, por lo que devolveré el empleado con menor edad.
	  
    // Min
    Optional<Employee> minAgeEmpOpt = employees.stream()
        .min(Comparator.comparing(Employee::age));
    Employee minAgeEmp = minAgeEmpOpt.get();
    System.out.println("Employee with minimum age is : " + minAgeEmp);
  	
	
Output:

Employee with minimum age is : Employee[name=Bob, age=25, salary=60000.0, company=Company B, skills=[JavaScript, React, Node.js]]

max

Nos devuelve el máximo elemento del stream en base a un criterio de comparación. En este caso le pesamos el criterio de edad, por lo que devolveré el empleado con mayor edad.
														
      // Max
    Optional<Employee> maxAgeEmpOpt = employees.stream()
        .max(Comparator.comparing(Employee::age));
    Employee maxAgeEmp = maxAgeEmpOpt.get();
    System.out.println("Employee with maximum age is : " + maxAgeEmp);
  												
  
Output:

Employee with maximum age is : Employee[name=Charlie, age=35, salary=50000.0, company=Company C, skills=[Java, Spring, SQL]]


Collectors

Y por último tenemos los maravillosos Collectors. 💣

Son estructuras modificables que reciben todos los elementos de un Stream y construyen un resultado como por ejemplo un Set, Map o un List.

Existen muchos Collectors que podemos utilizar. Incluso, podemos crear nuestros propios Collectors.

Algunos son los siguientes:
  • Collectors.groupingBy()
  • Collectors.averagingInt()
  • Collector.toList()
  • Collectors.toSet()
  • Collectors.toMap()
Los Collectors están compuestos por tres parámetros tipo Collector<T, A, R>. El primero representa el tipo de los elementos del Stream, el segundo representa el tipo que realiza la acumulación y el tercero representa el tipo de retorno.

El movimiento se demuestra andando. 🏃

En el primer ejemplo devolvemos los nombres de los empleados en un List:
												    
    // Collectors

    List<String> collectorList =
        employees.stream()
            .map(Employee::name)
            .collect(Collectors.toList());
    System.out.println(collectorList);




Output:
[Alice, Bob, Charlie]

En este otro ejemplo obtenemos un Map con el nombre de la compañia junto con la lista de empleados que la compone:


   
    List<Employee> allEmployees = List.of(
        new Employee("Alice", 30, 70000, "Company A", List.of("Java", "Spring", "SQL")),
        new Employee("Bob", 25, 60000, "Company B", List.of("JavaScript", "React", "Node.js")),
        new Employee("Charlie", 35, 50000, "Company C", List.of("Java", "Spring", "SQL")),
        new Employee("David", 28, 55000, "Company A", List.of("Java", "Spring", "SQL"))
    );

    // Group employees by company
    Map<String, List<Employee>> employeesByCompany = allEmployees.stream()
        .collect(Collectors.groupingBy(Employee::company));

    System.out.println(employeesByCompany);
                                                                                      
                                                  

Output:
{Company A=[Employee[name=Alice, age=30, salary=70000.0, company=Company A, skills=[Java, Spring, SQL]], Employee[name=David, age=28, salary=55000.0, company=Company A, skills=[Java, Spring, SQL]]], Company B=[Employee[name=Bob, age=25, salary=60000.0, company=Company B, skills=[JavaScript, React, Node.js]]], Company C=[Employee[name=Charlie, age=35, salary=50000.0, company=Company C, skills=[Java, Spring, SQL]]]}

¿Y si juntamos todo?

Vamos a mostrar un ejemplo con varias operaciones intermedias usando peek y reduce, además de mostrar el paralelismo que es una de las ventajas de usar streams en nuestro código.

El peek lo utilizo para hacer debug y revisar qué esta pasando en nuestras expresiones, aunque normalmente es una operación que no suele subir a entornos productivos. 👀👮.

El reduce es muy potente 🚀 y a partir del stream inicial, un elemento inicial y un bioperador ( operación que toma dos parámetros), genera un valor del mismo valor que el tipo del stream. 

En este ejemplo calculamos la suma de salarios 💰de los empleados, primero  obteniendo el stream de la forma habitual y a continuación obteniendo el stream para procesamiento paralelo. En este caso el stream es tan pequeño que no se notará la diferencia de rendimiento pero si lo hará en streams de mucho mayor tamaño. 
																	    // peek, reduce y parallel
   
          
	Double salary = employees.stream()
        .peek(System.out::println)
        .map(Employee::salary)
        .reduce(0.0, Double::sum);
    System.out.println("Sum of all salaries: " + salary);

    Double salaryParallel = employees.stream().parallel()
        .peek(System.out::println)
        .map(Employee::salary)
        .reduce(0.0, Double::sum);
    System.out.println("Sum of all salaries parallel way: " + salaryParallel);
																	
																



Output:

Employee[name=Alice, age=30, salary=70000.0, company=Company A, skills=[Java, Spring, SQL]]
Employee[name=Bob, age=25, salary=60000.0, company=Company B, skills=[JavaScript, React, Node.js]]
Employee[name=Charlie, age=35, salary=50000.0, company=Company C, skills=[Java, Spring, SQL]]
Sum of all salaries: 180000.0
Employee[name=Bob, age=25, salary=60000.0, company=Company B, skills=[JavaScript, React, Node.js]]
Employee[name=Charlie, age=35, salary=50000.0, company=Company C, skills=[Java, Spring, SQL]]
Employee[name=Alice, age=30, salary=70000.0, company=Company A, skills=[Java, Spring, SQL]]
Sum of all salaries parallel way: 180000.0


En el siguiente artículo vemos como migrar código tradicional a streams y algunos hacks muy útiles. 💪


Fuentes: jignect
	

Comentarios

Entradas populares de este blog

Soluciones Alchemy Classic 389 elementos

Hace algún tiempo salió una actualización del Juego Alchemy Classic en la que aparecían más elementos (389 en lugar de 238). Aparte de añadir elementos mejoran algunas traducciones en castellano y mejoran la interfaz, aunque todavía hay algún error en algunos nombres de elementos. Aquí os dejo las soluciones para los que estén atascados y no puedan dormir por las noches: Sustancia primaria Aire=Elemento primario  Fuego=Elemento primario  Agua=Elemento primario  Tierra=Sustancia Primaria Arena=Piedra + Aire Piedra=Tierra + Fuego Arcilla=Arena + Pantano Caliza=Tierra + Amonitas Carbono=Fuego + Madera Cloro=Fuego + Sal + Electricidad CO2(Dióxido de Carbono)=Ceniza + Ácido nítrico Electricidad=Relámpago+ Metales Gas natural= Yacimiento de gas + Pozo Helio=Refinería de gas + Gas Natural Hidrógeno=Electricidad + Agua Hielo=Frío + Agua Imán=Piedra + Metales Metano=Deshechos Vegetales + Pantano Oxígeno=Electricidad + Agua Pe...

Soluciones Alchemy Classic 442 elementos

Después de la resaca navideña y de la cuesta de enero, volvemos para informar la agradable sorpresa que nos ha dado a los fans de Alchemy Classic la empresa NIASOF ,  tras actualizar el juego Alchemy Classic. Una nueva versión con 442 elementos , interfaz mejorada de grupos y lo más importante, nuevos elementos que descubrir. La gran novedad de esta actualización son los puntos que tienes asignados , con los que puedes  conseguir pistas sobre los elementos que no has abierto todavía como: Conseguir un subelemento de un elemento, con 100 puntos . Conseguir el grupo de un subelemento de un elemento (qué lío , jeje), con 35 puntos . Me gusta, me gusta el enfoque de esta nueva versión aunque los elementos que han sacado me parecen poco originales. Parece que se van agotando las ideas para los elementos nuevos. Aquí van las soluciones: Carbon = Tierra + Turba Sol = Estrella + Tierra Espacio = 3 x Estrella Estrella = Helio + Hidrógeno Oso Pa...

Alchemy Classic

Dentro de los juegos que he descargado con el Android Market hay dos que destacan sobre todos los demás. El primero es Angry Birds (pájaros furiosos), en el cual comandas un ejercito de pájaros para luchar con una piara de cerdos malotes los cuales han robado sus huevos. Básicamente, tienes que ir pasando nivel tras nivel afinando tu puntería lanzando los pájaros a los cerdos que se esconden tras maderas, cristales y piedras. Pero la verdadera joya que no es tan conocida es Alchemy Classic . En este juego puedes desempeñar el papel  de Dios. Comenzando con 4 elementos básicos, Aire, Agua, Tierra y Fuego tienes que ir obteniendo todos los demás elementos de la creación (animales, plantas, herramientas...) mediante la combinación de los 4 primeros. En total hay 238 elementos. Después de una semana y alguna noche sin dormir por fin los he obtenido todos. El último elemento me ha llevado más de la cuenta por su maravillosa traducción. ¿Qué son abrojos? ¿Lo sabéis? Pues no, ...