Operaciones de Stream en Java: Intermedias y Finales
Clase 25 de 39 • Curso de Programación Funcional con Java SE
Contenido del curso
Entendiendo las partes de la programación funcional
- 2

Funciones en Programación Funcional: Conceptos Básicos
03:15 min - 3

Funciones como Ciudadanos de Primera Clase en Programación
02:39 min - 4

Funciones Puras: Concepto y Ejemplos Prácticos
04:05 min - 5

Efectos Secundarios en Programación y su Impacto en el Código
03:12 min - 6

Funciones de Orden Superior en Programación
02:28 min - 7

Funciones Lambda en Programación Funcional Java
03:30 min - 8

Inmutabilidad de Datos en Programación Funcional con Java
11:16 min
Functional Programming en Java
- 9

Programación Funcional en Java SE: Conceptos y Prácticas
00:06 min - 10

Programación Funcional en Java: Práctica y Ejemplos en IntelliJ
02:48 min - 11

Programación Funcional en Java: Creación y Uso de Funciones
04:03 min - 12

Programación Funcional con Predicados en Java
04:57 min - 13

Interfaz Consumer y Supplier en Java: Uso y Ejemplos Prácticos
03:54 min - 14

Funciones Java para Transformación de Datos y Operaciones Binarias
07:10 min - 15

Creación y Uso de Interfaces Funcionales Personalizadas en Java
08:51 min - 16

Métodos de Referencia en Programación Funcional Java
04:46 min - 17

Inferencia de tipos en Java: funciones y métodos
03:53 min - 18

Uso de Expresiones Lambda en Java: Sintaxis y Aplicaciones
12:46 min - 19

Interfaz Funcional en Java: Creación y Uso de Métodos Default
04:59 min - 20

Encadenamiento de Llamadas en Programación Orientada a Objetos
03:52 min - 21

Composición de Funciones en Programación Funcional
06:06 min
Optional y Streams: Datos mas interesantes
- 22

Uso de la Clase Optional en Java para Manejo de Valores Nulos
12:59 min - 23

Manipulación de Streams en Java: Operaciones y Limitaciones
10:18 min - 24

Programación Funcional en Java: Uso de Streams y Operaciones Terminales
07:21 min - 25

Operaciones de Stream en Java: Intermedias y Finales
Viendo ahora - 26

Operaciones y Concurrente con Stream en Java
05:51 min - 27

Operaciones Terminales en Java Streams
06:18 min - 28

Operaciones Intermedias en Streams de Java
09:21 min - 29

Conversión de Strings a Listas de Enteros en Java
06:14 min
Todo junto: Proyecto Job-search
- 30

Construcción de Proyecto para Buscar Empleo Usando APIs
01:17 min - 31

Configuración y Uso de Gradle en Proyectos Java con IntelliJ
03:23 min - 32

Creación de una Herramienta de Búsqueda de Trabajo en Terminal
01:51 min - 33

Creación de Puntos de Entrada y Dependencias en Proyectos Java
05:53 min - 34

Creación de APIs RESTful con Feign y Spring Boot
09:30 min - 35

Creación de una Interfaz de Línea de Comandos con JCommander
13:05 min - 36

Validación de Argumentos en Terminal con Clases en Ciel
04:31 min - 37

Procesamiento de Argumentos y Solicitudes API en Java
11:38 min - 38

Creación de API para búsqueda de empleos con Java y CLI
08:31 min
Conclusiones
Usando Stream nos podemos simplificar algunas operaciones, como es el filtrado, el mapeo, conversiones y más. Sin embargo, no es del todo claro cuándo una operación nos devuelve otro Stream para trabajar y cuándo nos da un resultado final…
¡O al menos no era claro hasta ahora!
Cuando hablamos de pasar lambdas a una operación de Stream, en realidad, estamos delegando a Java la creación de un objecto basado en una interfaz.
Por ejemplo:
Stream<String> coursesStream = Utils.getListOf("Java", "Node.js", "Kotlin").stream(); Stream<String> javaCoursesStream = coursesStream.filter(course -> course.contains("Java")); // En realidad, es lo mismo que: Stream<String> explicitOperationStream = coursesStream.filter(new Predicate<String>() { public boolean test(String st) { return st.contains("Java"); } });
Estas interfaces las mencionamos en clases anteriores. Solo como repaso, listo algunas a continuación:
Consumer<T>: recibe un dato de tipoTy no genera ningún resultadoFunction<T,R>: toma un dato de tipoTy genera un resultado de tipoRPredicate<T>: toma un dato de tipoTy evalúa si el dato cumple una condiciónSupplier<T>: no recibe ningún dato, pero genera un dato de tipoTcada vez que es invocadoUnaryOperator<T>recibe un dato de tipoTy genera un resultado de tipoT
Estas interfaces (y otras más) sirven como la base de donde generar los objetos con las lambdas que pasamos a los diferentes métodos de Stream. Cada una de ellas cumple esencialmente con recibir el tipo de dato de el Stream y generar el tipo de retorno que el método espera.
Si tuvieras tu propia implementación de Stream, se vería similar al siguiente ejemplo:
public class PlatziStream<T> implements Stream { private List<T> data; public Stream<T> filter(Predicate<T> predicate) { List<T> filteredData = new LinkedList<>(); for(T t : data){ if(predicate.test(t)){ filteredData.add(t); } } return filteredData.stream(); } }
Probablemente, tendría otros métodos y estructuras de datos, pero la parte que importa es justamente cómo se usa el Predicate. Lo que hace Stream internamente es pasar cada dato por este objeto que nosotros proveemos como una lambda y, según el resultado de la operación, decidir si debe incluirse o no en el Stream resultante.
Como puedes notar, esto no tiene mucha complejidad, puesto que es algo que pudimos fácilmente replicar. Pero Stream no solo incluye estas operaciones "triviales", también incluye un montón de utilidades para que la máquina virtual de Java pueda operar los elementos de un Stream de manera más rápida y distribuida.
Operaciones
A estas funciones que reciben lambdas y se encargan de trabajar (operar) sobre los datos de un Stream generalmente se les conoce como Operaciones.
Existen dos tipos de operaciones: intermedias y finales.
Cada operación aplicada a un Stream hace que el Stream original ya no sea usable para más operaciones. Es importante recordar esto, pues tratar de agregar operaciones a un Stream que ya esta siendo procesado es un error muy común.
En este punto seguramente te parezcan familiares todas estas operaciones, pues vienen en forma de métodos de la interfaz Stream. Y es cierto. Aunque son métodos, se les considera operaciones, puesto que su intención es operar el Stream y, posterior a su trabajo, el Stream no puede volver a ser operado.
En clases posteriores hablaremos más a detalle sobre cómo identificar una operación terminal de una operación intermedia.
Collectors
Una vez que has agregado operaciones a tu Stream de datos, lo más usual es que llegues a un punto donde ya no puedas trabajar con un Stream y necesites enviar tus datos en otro formato, por ejemplo, JSON o una List a base de datos.
Existe una interfaz única que combina todas las interfaces antes mencionadas y que tiene como única utilidad proveer de una operación para obtener todos los elementos de un Stream: Collector.
Collector<T, A, R> es una interfaz que tomará datos de tipo T del Stream, un tipo de dato mutable A, donde se iran agregando los elementos (mutable implica que podemos cambiar su contenido, como un LinkedList), y generara un resultado de tipo R.
Suena complicado… y lo es. Por eso mismo, Java 8 incluye una serie de Collectors ya definidos para no rompernos las cabeza con cómo convertir nuestros datos.
Veamos un ejemplo:
public List<String> getJavaCourses(Stream<String> coursesStream) { List<String> javaCourses = coursesStream.filter(course -> course.contains("Java")) .collect(Collectors.toList()); return javaCourses; }
Usando java.util.stream.Collectors podemos convertir muy sencillamente un Stream en un Set, Map, List, Collection, etc. La clase Collectors ya cuenta con métodos para generar un Collector que corresponda con el tipo de dato que tu Stream está usando. Incluso vale la pena resaltar que Collectors puede generar un ConcurrentMap que puede ser de utilidad si requieres de multiples threads.
Usar Collectors.toXXX es el proceso inverso de usar Collection.stream(). Esto hace que sea fácil generar APIs publicas que trabajen con estructuras/colecciones comunes e internamente utilizar Stream para agilizar las operaciones de nuestro lado.
Tipos de retorno
Hasta este punto, la única manera de obtener un dato que ya no sea un Stream es usando Collectors, pues la mayoría de operaciones de Stream se enfocan en operar los datos del Stream y generar un nuevo Stream con los resultados de la operación.
Sin embargo, algunas operaciones no cuentan con un retorno. Por ejemplo, forEach, que es una operación que no genera ningún dato. Para poder entender qué hace cada operación basta con plantear qué hace la operación para poder entender qué puede o no retornar.
Por ejemplo:
La operación de
findAnytrata de encontrar cualquier elemento que cumpla con la condición delPredicateque le pasamos como parámetro. Sin embargo, la operación dice que se devuelve unOptional. ¿Qué pasa cuando no encuentra ningún elemento? ¡Claro, por eso devuelve unOptional! Porque podría haber casos en que ningún elemento delStreamcumpla la condición.
En las clases posteriores haremos un listado más a detalle y con explicaciones de qué tipos de retorno tiene cada operación. Y entenderemos por qué se categorizan como operaciones finales e intermedias.
Conclusiones
Por ahora, hemos entendido que cada operación en un Stream consume hasta agotar el Stream. Y lo hace en un objeto no reusable. Esto implica que tenemos que decidir en nuestro código cuándo un Stream es un elemento temporal para una función o cuándo realmente una función sera la última en tocar los datos del Stream.
Las siguientes clases y lecturas cubrirán mas a detalle las múltiples operaciones y cómo afectan a los datos del Stream.