Aprenda a usar expresiones lambda para reducir en gran medida el desorden de código.
Las colecciones de números, cadenas y objetos se utilizan con tanta frecuencia en Java que eliminar incluso una pequeña cantidad de ceremonia al codificarlos puede reducir enormemente el desorden del código. En este artículo de dos partes, demuestro cómo usar expresiones lambda para aprovechar el estilo funcional de programación para crear un código más expresivo y conciso con menos mutabilidad y menos errores.
Después de leer este artículo, es posible que su código Java para manipular colecciones nunca vuelva a ser el mismo: será conciso, expresivo, elegante y más extensible que nunca.
Iterando a través de una lista
Iterar a través de una lista es una operación básica en una colección, pero a lo largo de los años esa operación ha experimentado algunos cambios significativos. Comenzaré con lo antiguo y desarrollaré un ejemplo, enumerando una lista de nombres, hasta el estilo elegante.
Puede crear fácilmente una colección inmutable de una lista de nombres con el siguiente código:
final List<String> friends =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
Esta es la forma habitual, pero no tan deseable, de iterar e imprimir cada uno de los elementos.
for(int i = 0; i < friends.size(); i++) { System.out.println(friends.get(i)); }
A este estilo lo llamo el patrón de heridas autoinfligidas: es verboso y propenso a errores. Tienes que detenerte y preguntarte: “¿Es
Java ofrece una construcción que es un poco más civilizada que el viejo
for(String name : friends) { System.out.println(name); }
Bajo el capó, esta forma de iteración usa la
Ambas versiones son iteradores externos , que mezclan cómo lo hace con lo que le gustaría lograr. Usted controla explícitamente la iteración con ellos, indicando dónde comenzar y dónde terminar; la segunda versión lo hace bajo el capó usando los
La segunda construcción tiene menos ceremonia que la primera y es mejor que la primera si no tiene la intención de modificar la colección en un índice particular. Sin embargo, ambos estilos son imperativos y puede prescindir de ellos en Java moderno utilizando un enfoque funcional .
Hay bastantes razones para favorecer el cambio al estilo funcional.
-
Los bucles son inherentemente secuenciales y son bastante difíciles de paralelizar.for
- Dichos bucles no son polimórficos: obtienes exactamente lo que pides. Pasó la colección a en
lugar de invocar un método (una operación polimórfica) en la colección para realizar la tarea.for
- A nivel de diseño, el código falla en el principio de “Diga, no pregunte”. Solicita que se realice una iteración específica en lugar de dejar los detalles de la iteración a las bibliotecas subyacentes.
Es hora de cambiar el antiguo estilo imperativo por la versión de estilo funcional más elegante de la iteración interna . Con una iteración interna, voluntariamente pasa el cómo a la biblioteca subyacente para que pueda concentrarse en el qué esencial . La función subyacente se encargará de gestionar la iteración.
Aquí, utilizará un iterador interno para enumerar los nombres. La
friends.forEach(new Consumer<String>() {
public void accept(final String name) {
System.out.println(name);
}
});
Ha invocado
Mire el resultado de esta versión, que es el mismo que el resultado de las dos versiones anteriores.
Brian
Nate
Neal
Raju
Sara
Scott
Cambió simplemente una cosa, intercambiando el
friends.forEach((final String name) -> System.out.println(name));
Eso es mucho mejor, y no solo porque es más corto. El
Esta versión produce el mismo resultado que las versiones anteriores. La versión del iterador interno es más concisa que las otras. Además, ayuda a centrar su atención en lo que desea lograr para cada elemento en lugar de en cómo secuenciar la iteración: es declarativo.
Sin embargo, la versión con mejor código tiene una limitación. Una vez
La sintaxis estándar para las expresiones lambda espera que los parámetros estén entre paréntesis, con la información de tipo proporcionada y separados por comas. El compilador de Java también ofrece cierta indulgencia y puede inferir los tipos. Omitir el tipo es conveniente, requiere menos esfuerzo y es menos ruidoso. Aquí está el código anterior sin la información del tipo.
friends.forEach((name) -> System.out.println(name));
En este caso, el compilador de Java determina que el
El compilador de Java trata las expresiones lambda de un solo parámetro como especiales. Puede omitir los paréntesis alrededor del parámetro si se infiere el tipo del parámetro.
friends.forEach(name -> System.out.println(name));
Hay una advertencia: los parámetros inferidos no son
Este ejemplo ha reducido bastante el código. Un último paso sacará otra onza de concisión.
friends.forEach(System.out::println);
Este código usa una referencia de método . Java le permite simplemente reemplazar el cuerpo del código con el nombre del método de su elección. Profundizaré en esto en la siguiente sección, pero por ahora reflexionemos sobre las sabias palabras de Antoine de Saint-Exupéry: “La perfección se logra no cuando no hay nada más que agregar, sino cuando no queda nada que quitar. “
Las expresiones lambda lo ayudaron a iterar de manera concisa sobre una colección. A continuación, verá cómo ayudan a eliminar la mutabilidad y hacen que el código sea aún más conciso cuando transforma colecciones.
Transformar una lista
Manipular una colección para producir otro resultado es tan fácil como iterar a través de los elementos de una colección. Suponga que la tarea consiste en convertir una lista de nombres en letras mayúsculas. ¿Cuáles son algunas opciones?
Java
Es mejor crear una nueva lista que tenga los elementos en mayúsculas.
Esa sugerencia puede parecer bastante ingenua al principio; el rendimiento es una preocupación obvia para todos. Sin embargo, es probable que descubra que el enfoque funcional a menudo produce un rendimiento sorprendentemente mejor que el enfoque imperativo. Comience creando una nueva colección de nombres en mayúsculas de la colección dada.
final List<String> uppercaseNames =
new ArrayList<String>();
for(String name : friends) {
uppercaseNames.add(name.toUpperCase());
}
En este estilo imperativo, este código creó una lista vacía y luego la llenó con nombres en mayúsculas, un elemento a la vez, mientras recorría la lista original. Como primer paso para avanzar hacia un estilo funcional, use el
final List<String> uppercaseNames =
new ArrayList<String>();
friends.forEach(name ->
uppercaseNames.add(name.toUpperCase()));
System.out.println(uppercaseNames);
Este código usaba el iterador interno, pero eso aún requería la lista vacía y el esfuerzo de agregarle elementos.
Yendo al siguiente paso, el
El
friends.stream()
.map(name -> name.toUpperCase())
.forEach(name -> System.out.print(name + " "));
System.out.println();
El método stream () está disponible en todas las colecciones desde JDK 8 y envuelve la colección en una instancia de
BRIAN NATE NEAL RAJU SARA SCOTT
El
En este ejemplo, tanto la entrada como la salida son una colección de cadenas. Podría haber pasado al
friends.stream()
.map(name -> name.length())
.forEach(count -> System.out.print(count + " "));
El resultado es un recuento del número de letras de cada nombre.
5 4 4 4 4 5
Las versiones que utilizan las expresiones lambda no tienen ninguna mutación explícita; son concisos. Estas versiones tampoco necesitaban ninguna colección vacía inicial o variable de basura; esa variable retrocedió silenciosamente en las sombras de la implementación subyacente.
Usar referencias de métodos
Puede hacer que el código sea un poco más conciso utilizando una función llamada referencia de método . El compilador de Java tomará una expresión lambda o una referencia a un método donde se espera una implementación de una interfaz funcional. Con esta característica, un corto
friends.stream()
.map(String::toUpperCase)
.forEach(name -> System.out.println(name));
Java sabe invocar el
En el ejemplo anterior, la referencia al método era para un método de instancia. Las referencias a
Las expresiones lambda ayudaron a enumerar una colección y a transformarla en una nueva colección. Las lambdas también pueden ayudarlo a elegir de manera concisa un elemento de una colección, a continuación.
¿Cuándo debería utilizar referencias de métodos? Por lo general, uso expresiones lambda con mucha más frecuencia que referencias a métodos cuando programo en Java. Sin embargo, eso no significa que las referencias a métodos no sean importantes o menos útiles. Son buenos reemplazos cuando las expresiones lambda son cortas y hacen llamadas directas y simples a un método de instancia o un método estático. En otras palabras, si las expresiones lambda simplemente pasan sus parámetros, puede reemplazarlos con referencias a métodos.
Estas expresiones lambda candidatas son muy parecidas a las de Tom Smykowski, en la película Office Space , cuyo trabajo es “tomar especificaciones de los clientes y llevarlas a los ingenieros de software”. Por esta razón, llamo a la refactorización de lambdas a referencias de métodos al patrón de espacio de oficina .
Además de la concisión, con las referencias a métodos se obtiene la capacidad de utilizar más directamente los nombres ya elegidos para estos métodos.
Hay bastante magia de compilación bajo el capó con referencias de métodos. El objeto y los parámetros de destino de la referencia del método se derivan de los parámetros pasados al método sintetizado. Esto hace que el código con referencias a métodos sea mucho más conciso que el código con expresiones lambda. Sin embargo, no puede usar esta conveniencia si la lógica de la aplicación requiere manipular parámetros antes de enviarlos como argumentos o modificar los resultados de la llamada antes de devolverlos.
Encontrar elementos en una colección
Los elegantes métodos utilizados para atravesar y transformar colecciones no le ayudarán directamente a elegir elementos de una colección. El
Imagínese que a partir de una lista de nombres, es necesario escoger los que empiezan con la letra N . Debido a que puede haber cero nombres coincidentes en la lista, el resultado puede ser una lista vacía. Primero, aquí le mostramos cómo codificarlo usando el enfoque anterior.
final List<String> startsWithN =
new ArrayList<String>();
for(String name : friends) {
if(name.startsWith("N")) {
startsWithN.add(name);
}
}
Es un código hablador para una tarea sencilla; creó una variable y la inicializó en una colección vacía. Luego recorrió la colección en busca de un nombre que comience con la letra deseada. Si lo encuentra, agrega el elemento a la colección.
Aquí se explica cómo refactorizar este código para usar el
final List<String> startsWithN =
friends.stream()
.filter(name -> name.startsWith("N"))
.collect(Collectors.toList());
El
A continuación, se explica cómo imprimir el número de elementos de la colección de resultados.
System.out.println(
String.format(
"Found %d names", startsWithN.size()));
A partir de la siguiente salida, está claro que el método recogió la cantidad adecuada de elementos de la colección de entrada:
Found 2 names
El
Conclusión
La concisión lograda mediante el uso de expresiones lambda hasta ahora es buena, pero la duplicación de código puede colarse rápidamente si no tiene cuidado. Abordaré esta preocupación en la segunda parte de este artículo, “Programación funcional en Java, Parte 2: reutilización de Lambda, alcance léxico y cierres, y reduce ()”.
Fuente: https://blogs.oracle.com/javamagazine/java-functional-programming-lambda-method-references