Capítulo 2: Uso de Colecciones
A menudo utilizamos varias colecciones, números, cadenas y objetos. Están en todas partes, e incluso si el código que opera la colección se puede optimizar ligeramente, el código será mucho más claro. En este capítulo, exploramos cómo utilizar expresiones lambda para manipular colecciones. Lo usamos para recorrer colecciones, convertir colecciones en nuevas colecciones, eliminar elementos de colecciones y fusionar colecciones.
recorrer la lista
Recorrer una lista es la operación de conjunto más básica y sus operaciones han sufrido algunos cambios a lo largo de los años. Usamos un pequeño ejemplo de recorrido de nombres, presentándolo desde la versión más antigua hasta la versión más elegante de la actualidad.
Podemos crear fácilmente una lista inmutable de nombres con el siguiente código:
Copie el código de código de la siguiente manera:
Lista final<String> amigos =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
System.out.println(amigos.get(i));
}
El siguiente es el método más común para recorrer una lista e imprimirla, aunque también es el más general:
Copie el código de código de la siguiente manera:
for(int i = 0; i < amigos.tamaño(); i++) {
System.out.println(amigos.get(i));
}
A esta forma de escribir la llamo masoquista: es detallada y propensa a errores. Tenemos que detenernos y pensar en ello: "¿Es i< o i<=?". Esto sólo tiene sentido cuando necesitamos operar en un elemento específico, pero incluso entonces, aún podemos usar expresiones funcionales que se adhieran al principio de estilo de inmutabilidad, que discutiremos en breve.
Java también proporciona una estructura relativamente avanzada.
Copie el código de código de la siguiente manera:
colecciones/fpij/Iteration.java
para (nombre de cadena: amigos) {
System.out.println(nombre);
}
En el fondo, la iteración de esta manera se implementa utilizando la interfaz Iterator, llamando a sus métodos hasNext y next. Ambos métodos son iteradores externos y combinan cómo hacerlo con lo que quieres hacer. Controlamos explícitamente la iteración, diciéndole dónde comenzar y dónde terminar; la segunda versión lo hace bajo el capó a través del método Iterator. Bajo operación explícita, también puede usar declaraciones break y continue para controlar la iteración. A la segunda versión le faltan algunas cosas de la primera. Este enfoque es mejor que el primero si no pretendemos modificar ningún elemento de la colección. Sin embargo, ambos métodos son imperativos y deberían abandonarse en el Java actual. Hay varias razones para cambiar al estilo funcional:
1. El bucle for en sí es en serie y difícil de paralelizar.
2. Un bucle así no es polimórfico; lo que obtienes es lo que pides. Pasamos la colección directamente al bucle for, en lugar de llamar a un método (que admite polimorfismo) en la colección para realizar una operación específica.
3. Desde una perspectiva de diseño, el código escrito de esta manera viola el principio de "decir, no preguntar". Solicitamos que se realice una iteración en lugar de dejarla en manos de la biblioteca subyacente.
Es hora de pasar de la antigua programación imperativa a la programación funcional más elegante de iteradores internos. Después de usar iteradores internos, dejamos muchas operaciones específicas a la biblioteca de métodos subyacente para su ejecución, para que pueda concentrarse más en requisitos comerciales específicos. La función subyacente será responsable de la iteración. Primero usamos un iterador interno para enumerar la lista de nombres.
La interfaz Iterable ha sido mejorada en JDK8. Tiene un nombre especial llamado forEach, que recibe un parámetro de tipo Comsumer. Como su nombre lo indica, una instancia de consumidor consume el objeto que se le pasa a través de su método de aceptación. Usamos la familiar sintaxis de clase interna anónima para usar el método forEach:
Copie el código de código de la siguiente manera:
friends.forEach(new Consumer<String>() { public void aceptar(nombre de cadena final) {
System.out.println(nombre);
});
Llamamos al método forEach en la colección de amigos y le pasamos una implementación anónima de Consumer. Este método forEach llama al método de aceptación del consumidor pasado para cada elemento de la colección, lo que le permite procesar este elemento. En este ejemplo simplemente imprimimos su valor, que es el nombre. Echemos un vistazo al resultado de esta versión, que es el mismo que las dos anteriores:
Copie el código de código de la siguiente manera:
brian
nata
neal
raju
sara
Scott
Solo cambiamos una cosa: abandonamos el bucle for obsoleto y utilizamos un nuevo iterador interno. La ventaja es que no tenemos que especificar cómo iterar la colección y podemos centrarnos más en cómo procesar cada elemento. La desventaja es que el código parece más detallado, lo que casi acaba con la alegría del nuevo estilo de codificación. Afortunadamente, esto es fácil de cambiar y aquí es donde entra en juego el poder de las expresiones lambda y los nuevos compiladores. Hagamos una modificación más y reemplacemos la clase interna anónima con una expresión lambda.
Copie el código de código de la siguiente manera:
friends.forEach((nombre de cadena final) -> System.out.println(nombre));
Se ve mucho mejor así. Hay menos código, pero primero veamos lo que eso significa. El método forEach es una función de orden superior que recibe una expresión lambda o un bloque de código para operar con los elementos de la lista. En cada llamada, los elementos de la colección estarán vinculados a la variable de nombre. La biblioteca subyacente alberga la actividad de invocación de expresiones lambda. Puede decidir retrasar la ejecución de expresiones y, si corresponde, realizar cálculos paralelos. El resultado de esta versión también es el mismo que el de la anterior.
Copie el código de código de la siguiente manera:
brian
nata
neal
raju
sara
Scott
La versión del iterador interno es más concisa. Además, al usarlo, podemos centrarnos más en el procesamiento de cada elemento en lugar de atravesarlo; esto es declarativo.
Sin embargo, esta versión tiene fallas. Una vez que el método forEach comienza a ejecutarse, a diferencia de las otras dos versiones, no podemos salir de esta iteración. (Por supuesto que hay otras formas de hacer esto). Por lo tanto, esta forma de escritura se usa más comúnmente cuando es necesario procesar cada elemento de la colección. Más adelante introduciremos algunas otras funciones que nos permiten controlar el proceso del bucle.
La sintaxis estándar para las expresiones lambda es colocar los parámetros dentro de (), proporcionar información de tipo y usar comas para separar los parámetros. Para liberarnos, el compilador de Java también puede realizar automáticamente la deducción de tipos. Por supuesto que es más conveniente no escribir tipos. Hay menos trabajo y el mundo está más tranquilo. La siguiente es la versión anterior después de eliminar el tipo de parámetro:
Copie el código de código de la siguiente manera:
amigos.forEach((nombre) -> System.out.println(nombre));
En este ejemplo, el compilador de Java sabe que el tipo de nombre es Cadena mediante análisis de contexto. Observa la firma del método llamado forEach y luego analiza la interfaz funcional en los parámetros. Luego analizará el método abstracto en esta interfaz y verificará el número y tipo de parámetros. Incluso si esta expresión lambda recibe múltiples parámetros, aún podemos realizar la deducción de tipos, pero en este caso todos los parámetros no pueden tener tipos de parámetros en las expresiones lambda, los tipos de parámetros deben escribirse en absoluto o, si están escritos, deben escribirse; en su totalidad.
El compilador de Java trata especialmente las expresiones lambda con un único parámetro: si desea realizar una inferencia de tipos, se pueden omitir los paréntesis alrededor del parámetro.
Copie el código de código de la siguiente manera:
amigos.forEach(nombre -> System.out.println(nombre));
Aquí hay una pequeña advertencia: los parámetros utilizados para la inferencia de tipos no son del tipo final. En el ejemplo anterior de declaración explícita de un tipo, también marcamos el parámetro como final. Esto le impide cambiar el valor del parámetro en la expresión lambda. En términos generales, es un mal hábito modificar el valor de un parámetro, lo que fácilmente causará ERRORES, por lo que es un buen hábito marcarlo como final. Desafortunadamente, si queremos utilizar la inferencia de tipos, tenemos que seguir las reglas nosotros mismos y no modificar los parámetros, porque el compilador ya no nos protege.
Fue necesario mucho esfuerzo para llegar a este punto, pero ahora la cantidad de código es un poco menor. Pero esto aún no es lo más sencillo. Probemos esta última versión minimalista.
Copie el código de código de la siguiente manera:
amigos.forEach(System.out::println);
En el código anterior utilizamos una referencia de método. Podemos reemplazar directamente el código completo con el nombre del método. Exploraremos esto en profundidad en la siguiente sección, pero por ahora recordemos una cita famosa de Antoine de Saint-Exupéry: La perfección no es lo que se puede agregar, sino lo que ya no se puede quitar.
Las expresiones lambda nos permiten recorrer colecciones de forma concisa y clara. En la siguiente sección, hablaremos sobre cómo nos permite escribir un código tan conciso al realizar operaciones de eliminación y conversiones de colecciones.