|
| 1 | +package com.platzi.functional_student_practice._15_streams; |
| 2 | + |
| 3 | +import com.platzi.functional_teacher_theory.util.Utils; |
| 4 | + |
| 5 | +import java.util.List; |
| 6 | +import java.util.function.Predicate; |
| 7 | +import java.util.stream.Collectors; |
| 8 | +import java.util.stream.Stream; |
| 9 | + |
| 10 | +public class C25CollectorsTheory { |
| 11 | + |
| 12 | + /* |
| 13 | +
|
| 14 | + Operaciones y Collectors |
| 15 | +
|
| 16 | + Usando Stream nos podemos simplificar algunas operaciones, como es el filtrado, el mapeo, |
| 17 | + conversiones y más. Sin embargo, no es del tod0 claro cuándo una operación nos devuelve otro |
| 18 | + Stream para trabajar y cuándo nos da un resultado final… |
| 19 | +
|
| 20 | + ¡O al menos no era claro hasta ahora! |
| 21 | +
|
| 22 | + Cuando hablamos de pasar lambdas a una operación de Stream, en realidad, estamos delegando a |
| 23 | + Java la creación de un objecto basado en una interfaz. |
| 24 | +
|
| 25 | + Por ejemplo: |
| 26 | +
|
| 27 | + */ |
| 28 | + |
| 29 | + public static void main(String[] args) { |
| 30 | + |
| 31 | + Stream<String> coursesStream = Utils.getListOf("Java", "Node.js", "Kotlin").stream(); |
| 32 | + List<String> javaCoursesStream = coursesStream.filter(course -> course.contains("Java")).toList(); |
| 33 | + |
| 34 | + // En realidad, es lo mismo que: |
| 35 | + List<String> explicitOperationStream = coursesStream.filter(new Predicate<String>() { |
| 36 | + public boolean test(String st) { |
| 37 | + return st.contains("Java"); |
| 38 | + } |
| 39 | + }).toList(); |
| 40 | + } |
| 41 | + |
| 42 | + /* |
| 43 | +
|
| 44 | + Estas interfaces las mencionamos en clases anteriores. Solo como repaso, listo algunas a |
| 45 | + continuación: |
| 46 | +
|
| 47 | + Consumer<T>: recibe un dato de tipo T y no genera ningún resultado |
| 48 | + Function<T,R>: toma un dato de tipo T y genera un resultado de tipo R |
| 49 | + Predicate<T>: toma un dato de tipo T y evalúa si el dato cumple una condición |
| 50 | + Supplier<T>: no recibe ningún dato, pero genera un dato de tipo T cada vez que es invocado |
| 51 | + UnaryOperator<T> recibe un dato de tipo T y genera un resultado de tipo T |
| 52 | +
|
| 53 | + Estas interfaces (y otras más) sirven como la base de donde generar los objetos con las lambdas |
| 54 | + que pasamos a los diferentes métodos de Stream. Cada una de ellas cumple esencialmente con |
| 55 | + recibir el tipo de dato de el Stream y generar el tipo de retorno que el método espera. |
| 56 | +
|
| 57 | + Si tuvieras tu propia implementación de Stream, se vería similar al siguiente ejemplo: |
| 58 | +
|
| 59 | + public class PlatziStream<T> implements Stream { |
| 60 | + private List<T> data; |
| 61 | +
|
| 62 | + public Stream<T> filter(Predicate<T> predicate) { |
| 63 | + List<T> filteredData = new LinkedList<>(); |
| 64 | + for(T t : data){ |
| 65 | + if(predicate.test(t)){ |
| 66 | + filteredData.add(t); |
| 67 | + } |
| 68 | + } |
| 69 | +
|
| 70 | + return filteredData.stream(); |
| 71 | + } |
| 72 | + } |
| 73 | +
|
| 74 | + Probablemente, tendría otros métodos y estructuras de datos, pero la parte que importa es justamente |
| 75 | + cómo se usa el Predicate. Lo que hace Stream internamente es pasar cada dato por este objeto que |
| 76 | + nosotros proveemos como una lambda y, según el resultado de la operación, decidir si debe incluirse |
| 77 | + o no en el Stream resultante. |
| 78 | +
|
| 79 | + Como puedes notar, esto no tiene mucha complejidad, puesto que es algo que pudimos fácilmente replicar. |
| 80 | + Pero Stream no solo incluye estas operaciones “triviales”, también incluye un montón de utilidades |
| 81 | + para que la máquina virtual de Java pueda operar los elementos de un Stream de manera más rápida y |
| 82 | + distribuida. |
| 83 | +
|
| 84 | +
|
| 85 | +
|
| 86 | + Operaciones |
| 87 | +
|
| 88 | + A estas funciones que reciben lambdas y se encargan de trabajar (operar) sobre los datos de un Stream |
| 89 | + generalmente se les conoce como Operaciones. |
| 90 | +
|
| 91 | + Existen dos tipos de operaciones: intermedias y finales. |
| 92 | +
|
| 93 | + Cada operación aplicada a un Stream hace que el Stream original ya no sea usable para más operaciones. |
| 94 | + Es importante recordar esto, pues tratar de agregar operaciones a un Stream que ya esta siendo |
| 95 | + procesado es un error muy común. |
| 96 | +
|
| 97 | + En este punto seguramente te parezcan familiares todas estas operaciones, pues vienen en forma de |
| 98 | + métodos de la interfaz Stream. Y es cierto. Aunque son métodos, se les considera operaciones, |
| 99 | + puesto que su intención es operar el Stream y, posterior a su trabajo, el Stream no puede volver a |
| 100 | + ser operado. |
| 101 | +
|
| 102 | + En clases posteriores hablaremos más a detalle sobre cómo identificar una operación terminal de una |
| 103 | + operación intermedia. |
| 104 | +
|
| 105 | +
|
| 106 | +
|
| 107 | + Collectors |
| 108 | +
|
| 109 | + Una vez que has agregado operaciones a tu Stream de datos, lo más usual es que llegues a un punto |
| 110 | + donde ya no puedas trabajar con un Stream y necesites enviar tus datos en otro formato, por ejemplo, |
| 111 | + JSON o una List a base de datos. |
| 112 | +
|
| 113 | + Existe una interfaz única que combina todas las interfaces antes mencionadas y que tiene como única |
| 114 | + utilidad proveer de una operación para obtener todos los elementos de un Stream: Collector. |
| 115 | +
|
| 116 | + Collector<T, A, R> es una interfaz que tomará datos de tipo T del Stream, un tipo de dato mutable A, |
| 117 | + donde se iran agregando los elementos (mutable implica que podemos cambiar su contenido, como un |
| 118 | + LinkedList), y generara un resultado de tipo R. |
| 119 | +
|
| 120 | + Suena complicado… y lo es. Por eso mismo, Java 8 incluye una serie de Collectors ya definidos para |
| 121 | + no rompernos las cabeza con cómo convertir nuestros datos. |
| 122 | +
|
| 123 | + Veamos un ejemplo: |
| 124 | +
|
| 125 | + */ |
| 126 | + |
| 127 | + public List<String> getJavaCourses(Stream<String> coursesStream) { |
| 128 | + List<String> javaCourses = coursesStream |
| 129 | + .filter(course -> course.contains("Java")) |
| 130 | + .collect(Collectors.toList()); |
| 131 | + return javaCourses; |
| 132 | + } |
| 133 | + |
| 134 | + /* |
| 135 | +
|
| 136 | + Usando java.util.stream.Collectors podemos convertir muy sencillamente un Stream en un Set, Map, |
| 137 | + List, Collection, etc. La clase Collectors ya cuenta con métodos para generar un Collector que |
| 138 | + corresponda con el tipo de dato que tu Stream está usando. Incluso vale la pena resaltar que |
| 139 | + Collectors puede generar un ConcurrentMap que puede ser de utilidad si requieres de multiples |
| 140 | + threads. |
| 141 | +
|
| 142 | + Usar Collectors.toXXX es el proceso inverso de usar Collection.stream(). Esto hace que sea fácil |
| 143 | + generar APIs publicas que trabajen con estructuras/colecciones comunes e internamente utilizar |
| 144 | + Stream para agilizar las operaciones de nuestro lado. |
| 145 | +
|
| 146 | +
|
| 147 | +
|
| 148 | + Tipos de retorno |
| 149 | +
|
| 150 | + Hasta este punto, la única manera de obtener un dato que ya no sea un Stream es usando Collectors, |
| 151 | + pues la mayoría de operaciones de Stream se enfocan en operar los datos del Stream y generar un |
| 152 | + nuevo Stream con los resultados de la operación. |
| 153 | +
|
| 154 | + Sin embargo, algunas operaciones no cuentan con un retorno. Por ejemplo, forEach, que es una |
| 155 | + operación que no genera ningún dato. Para poder entender qué hace cada operación basta con |
| 156 | + plantear qué hace la operación para poder entender qué puede o no retornar. |
| 157 | +
|
| 158 | + Por ejemplo: |
| 159 | +
|
| 160 | + La operación de findAny trata de encontrar cualquier elemento que cumpla con la condición |
| 161 | + del Predicate que le pasamos como parámetro. Sin embargo, la operación dice que se devuelve |
| 162 | + un Optional. ¿Qué pasa cuando no encuentra ningún elemento? ¡Claro, por eso devuelve un |
| 163 | + Optional! Porque podría haber casos en que ningún elemento del Stream cumpla la condición. |
| 164 | +
|
| 165 | + En las clases posteriores haremos un listado más a detalle y con explicaciones de qué tipos de |
| 166 | + retorno tiene cada operación. Y entenderemos por qué se categorizan como operaciones finales e |
| 167 | + intermedias. |
| 168 | +
|
| 169 | +
|
| 170 | + Conclusiones |
| 171 | +
|
| 172 | + Por ahora, hemos entendido que cada operación en un Stream consume hasta agotar el Stream. |
| 173 | + Y lo hace en un objeto no reusable. Esto implica que tenemos que decidir en nuestro código |
| 174 | + cuándo un Stream es un elemento temporal para una función o cuándo realmente una función |
| 175 | + sera la última en tocar los datos del Stream. |
| 176 | +
|
| 177 | + Las siguientes clases y lecturas cubrirán mas a detalle las múltiples operaciones y cómo |
| 178 | + afectan a los datos del Stream. |
| 179 | +
|
| 180 | + */ |
| 181 | +} |
0 commit comments