Las lambdas crean un código más expresivo y conciso con menos mutabilidad y menos errores. En el primer artículo de esta serie de dos partes, demostré cómo las expresiones lambda aprovechan el poder del estilo funcional de programación en Java. En esta parte final, exploro esto más a fondo y considero una advertencia de precaución. (Le sugiero que lea Programación funcional en Java, Parte 1: Listas, lambdas y referencias de métodos , si aún no lo ha hecho).
Como verá, las expresiones lambda son engañosamente concisas y es fácil duplicarlas descuidadamente en el código. El código duplicado conduce a un código de mala calidad que es difícil de mantener; si necesita hacer un cambio, tendrá que buscar y tocar el código relevante en varios lugares.
Evitar la duplicación también puede ayudar a mejorar el rendimiento. Al mantener el código relacionado con un conocimiento concentrado en un solo lugar, puede estudiar fácilmente su perfil de rendimiento y realizar cambios en un solo lugar para obtener un mejor rendimiento.
Reutilizar expresiones lambda
Demostraré lo fácil que es caer en la trampa de la duplicación al usar expresiones lambda y sugeriré formas de evitar esa trampa. Suponga que tiene un par de colecciones de nombres:
final List<String> friends =
Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");
final List<String> editors =
Arrays.asList("Brian", "Jackie", "John", "Mike");
final List<String> comrades =
Arrays.asList("Kate", "Ken", "Nick", "Paula", "Zach");
El objetivo es filtrar los nombres que comienzan con una letra determinada. Primero, aquí hay un enfoque ingenuo para esta tarea utilizando el
final long countFriendsStartN =
friends.stream()
.filter(name -> name.startsWith("N"))
.count();
final long countEditorsStartN =
editors.stream()
.filter(name -> name.startsWith("N"))
.count();
final long countFriendsStartN =
friends.stream()
.filter(name -> name.startsWith("N"))
.count();
Las expresiones lambda hicieron que el código fuera conciso, pero silenciosamente condujeron a un código duplicado. En el ejemplo anterior, un cambio en la expresión lambda requiere cambios en más de un lugar, eso es un no-no. Afortunadamente, puede asignar expresiones lambda a variables y reutilizarlas, como lo haría con los objetos.
El
A continuación, se explica cómo refactorizar el código anterior para que se adhiera a las mejores prácticas DRY (Don’t Repeat Yourself).
final Predicate<String> startsWithN =
name -> name.startsWith("N");
final long countFriendsStartN =
friends.stream()
.filter(startsWithN)
.count();
final long countEditorsStartN =
editors.stream()
.filter(startsWithN)
.count();
final long countComradesStartN =
comrades.stream()
.filter(startsWithN)
.count();
En lugar de duplicar la expresión lambda varias veces, la creó una vez y la almacenó en una referencia con el nombre
La nueva variable eliminó suavemente la duplicación que se coló. Desafortunadamente, la duplicación está a punto de volver a colarse con fuerza, como verá a continuación, y necesita algo un poco más poderoso para frustrarla.
Uso de cerramientos y alcance léxico
Algunos desarrolladores creen que el uso de expresiones lambda podría generar duplicación y reducir la calidad del código. Contrariamente a esa creencia, incluso cuando el código se vuelve más complicado, no es necesario comprometer la calidad del código para disfrutar de la concisión proporcionada por las expresiones lambda.
Consiguió reutilizar la expresión lambda en el ejemplo anterior; sin embargo, la duplicación se infiltrará rápidamente cuando traiga una segunda carta para que coincida. Exploremos el problema más a fondo y luego lo resolveremos utilizando límites y cierres léxicos.
Duplicación en expresiones lambda. Escojamos los nombres que comienzan con N o B de la
final Predicate<String> startsWithN =
name -> name.startsWith("N");
final Predicate<String> startsWithB =
name -> name.startsWith("B");
final long countFriendsStartN =
friends.stream()
.filter(startsWithN)
.count();
final long countFriendsStartB =
friends.stream()
.filter(startsWithB)
.count();
Las primeras pruebas de predicado si el nombre comienza con una N y las segundas pruebas para un B . Pasas estas dos instancias a las dos llamadas al
Eliminando la duplicación usando el alcance léxico. Como primera opción, puede extraer la letra como parámetro de una función y pasar la función como argumento al
Para fines de comparación, necesita una variable que almacene la letra en caché para su uso posterior y la mantenga hasta que
public static Predicate<String>
checkIfStartsWith(final String letter) {
return name -> name.startsWith(letter);
}
Esta definida
El
Pero, ¿a qué está
Vale la pena señalar aquí que existen algunas restricciones para el alcance léxico. Por un lado, desde dentro de una expresión lambda, puede acceder solo a las variables locales que están
Para evitar condiciones de carrera, las variables locales a las que accede en el ámbito adjunto no pueden cambiar una vez que se inicializan. Cualquier intento de cambiarlos resultará en un error de compilación. Las variables marcadas
- Las variables a las que se accede deben inicializarse dentro de los métodos adjuntos antes de que se defina la expresión lambda.
- Los valores de estas variables no cambian en ningún otro lugar, es decir, están efectivamente
incluso si no están marcados como tales.final
Cuando usa expresiones lambda que capturan el estado local, también debe tener en cuenta que las expresiones lambda sin estado son constantes en tiempo de ejecución, pero aquellas que capturan el estado local tienen un costo de evaluación adicional.
Con estas restricciones en mente, veamos cómo usar la expresión lambda devuelta por
final long countFriendsStartN =
friends.stream()
.filter(checkIfStartsWith("N"))
.count();
final long countFriendsStartB =
friends.stream()
.filter(checkIfStartsWith("B"))
.count();
En las llamadas al
Al crear una función de orden superior
Refactorización para reducir el alcance. En el ejemplo anterior, usó un
final Function<String, Predicate<String>>
startsWithLetter = (String letter) -> {
Predicate<String> checkStarts =
(String name) -> name.startsWith(letter);
return checkStarts;
};
Esta expresión lambda reemplaza el
Esta versión es detallada en comparación con el
final Function<String, Predicate<String>>
startsWithLetter =
letter -> name -> name.startsWith(letter);
Ha completado el círculo con funciones de orden superior en esta sección. Los ejemplos ilustran cómo pasar funciones a funciones, crear funciones dentro de funciones y devolver funciones desde dentro de funciones. También demuestran la concisión y la capacidad de reutilización que facilitan las expresiones lambda.
Hizo un buen uso de ambos
- A
toma un parámetro de tipoPredicate<T>y devuelve unTresultado para indicar una decisión para cualquier verificación que represente. Puede usarlo en cualquier momento que desee tomar una decisión de aprobar o no aprobar para un candidato que pase alBoolean. Métodos comoPredicateel de evaluar elementos candidatos toman afilter()como parámetro.Predicate
- A
representa una función que toma un parámetro de tipoFunction <T, R>y devuelve un resultado de tipoT. Esto es más general que unRque siempre devuelve un booleano. Puede emplear un enPredicatecualquier lugar donde desee transformar una entrada en otro valor, por lo que es bastante lógico que elFunctionmétodo usemap()como parámetro.Function
Seleccionar todos los elementos coincidentes de una colección fue fácil. A continuación, le mostraré cómo elegir un solo elemento de una colección.
Escogiendo un solo elemento
Es razonable esperar que elegir un elemento de una colección sea más simple que elegir varios elementos, pero eso no siempre es cierto. Comenzaré explorando la complejidad introducida por el enfoque habitual y luego incorporaré expresiones lambda para reducir esa complejidad.
Creemos un método que busque un elemento que comience con una letra determinada y la imprima.
public static void pickName(
final List<String> names,
final String startingLetter) {
String foundName = null;
for(String name : names) {
if(name.startsWith(startingLetter)) {
foundName = name;
break;
}
}
System.out.print(
String.format(
"A name starting with %s: ",
startingLetter));
if(foundName != null) {
System.out.println(foundName);
} else {
System.out.println("No name found");
}
}
El olor de este método puede competir fácilmente con los camiones de basura que pasan. Primero creó una
Luego usó un iterador externo para recorrer los elementos, pero tuvo que salir del bucle si encontraba un elemento; aquí hay otras fuentes de olores rancios: obsesión primitiva, estilo imperativo y mutabilidad. Una vez fuera del ciclo, tenía que verificar la respuesta e imprimir el resultado apropiado. Eso es bastante código para una tarea simple.
Reconsideremos el problema. Simplemente desea elegir el primer elemento coincidente y lidiar con seguridad con la ausencia de dicho elemento. Reescribamos el
public static void pickName(
final List<String> names,
final String startingLetter) {
final Optional<String> foundName =
names.stream()
.filter(name -> name.startsWith(startingLetter))
.findFirst();
System.out.println(
String.format( "A name starting with %s: %s",
startingLetter, foundName.orElse("No name found")));
}
Algunas funciones poderosas en la biblioteca JDK se unieron para ayudar a lograr esta concisión. Primero, el
La
Puede consultar si un objeto está presente utilizando el
pickName(friends, "N");
pickName(friends, "Z");
El código selecciona el primer elemento coincidente, si se encuentra uno, e imprime un mensaje apropiado en caso contrario.
A name starting with N: Nate
A name starting with Z: No name found
La combinación del
foundName.ifPresent( name ->
System.out.println("Hello " + name));
En comparación con el uso de la versión imperativa para elegir el primer nombre coincidente, el estilo funcional agradable y fluido se ve mejor. Pero, ¿está haciendo más trabajo en la versión fluida que en la versión imperativa? La respuesta es no: estos métodos tienen la inteligencia para realizar solo el trabajo necesario.
La búsqueda del primer elemento coincidente demostró algunas capacidades más ordenadas en el JDK. A continuación, le mostraré cómo las expresiones lambda ayudan a calcular un solo resultado de una colección.
Reducir una colección a un solo valor
He repasado bastantes técnicas para manipular colecciones hasta ahora: elegir elementos coincidentes, seleccionar un elemento en particular y transformar una colección. Todas estas operaciones tienen una cosa en común: todas trabajaron de forma independiente en elementos individuales de la colección. Ninguno requirió comparar elementos entre sí o transferir cálculos de un elemento al siguiente.
En esta sección, compararé elementos y transferiré un estado computacional a través de una colección. Esto comenzará con algunas operaciones básicas y luego se desarrollará hasta algo un poco más sofisticado. Como primer ejemplo, leerá los valores de la
System.out.println(
"Total number of characters in all names: " +
friends.stream()
.mapToInt(name -> name.length())
.sum());
Para encontrar el total de caracteres, necesita la longitud de cada nombre, que se puede encontrar usando el
Total number of characters in all names: 26
Este código aprovechó el
En lugar de usar el
El encanto oculto en el ejemplo anterior es el patrón MapReduce cada vez más popular, siendo el
Por ejemplo, la tarea consiste en leer la colección de nombres dada y mostrar el más largo. Si hay más de un nombre con la misma longitud más larga, mostrará el primero que encuentre. Una forma de hacerlo es averiguar la longitud más larga y luego elegir el primer elemento de esa longitud. Pero eso requeriría revisar la lista dos veces, lo cual no es eficiente. Aquí es donde
Puede utilizar el
final Optional<String> aLongName =
friends.stream()
.reduce((name1, name2) ->
name1.length() >= name2.length() ? name1 : name2);
aLongName.ifPresent(name ->
System.out.println(
String.format("A longest name: %s", name)));
La expresión lambda que está pasando al
Esta expresión lambda se ajusta a la interfaz de un
A longest name: Brian
A medida que el
El resultado del
Del ejemplo, puede inferir que el
final String steveOrLonger =
friends.stream()
.reduce("Steve", (name1, name2) ->
name1.length() >= name2.length() ? name1 : name2);
Si algún nombre es más largo que la base dada, se recoge; de lo contrario, la función devuelve el valor base, que es “Steve” en este ejemplo. Esta versión de
Antes de terminar, visitemos una operación fundamental, pero aparentemente difícil, sobre colecciones: unir elementos.
Uniendo elementos
Ha explorado cómo seleccionar elementos, iterar y transformar colecciones. Sin embargo, en una operación trivial, concatenando una colección, podrías perder todas las ganancias obtenidas con un código conciso y elegante si no fuera por la
Volviendo a la
for(String name : friends) {
System.out.print(name + ", ");
}
System.out.println();
Ese código simple produce lo siguiente:
Brian, Nate, Neal, Raju, Sara, Scott,
¡UH oh! Hay una coma maloliente al final (¿se puede culpar a Scott?). ¿Cómo le dices a Java que no coloque una coma allí? Desafortunadamente, el ciclo seguirá su curso y no hay una manera fácil de distinguir el último elemento del resto. Para solucionar este problema, puede volver al bucle habitual.
for(int i = 0; i < friends.size() - 1; i++) {
System.out.print(friends.get(i) + ", ");
}
if(friends.size() > 0)
System.out.println(friends.get(friends.size() - 1));
Veamos si el resultado de esta versión es decente.
Brian, Nate, Neal, Raju, Sara, Scott
La salida se ve bien, pero el código para producir la salida no. Transpórtame, Java moderno.
Buenas noticias: la
System.out.println(String.join(", ", friends));
Lo siguiente verifica que el resultado sea tan encantador como el código que lo produjo:
Brian, Nate, Neal, Raju, Sara, Scott
Bajo el capó, el
También puede transformar los elementos antes de unirlos. Ya sabes cómo transformar elementos usando el
Puede usar el
El
System.out.println(
friends.stream()
.map(String::toUpperCase)
.collect(joining(", ")));
Este código invocó el
Aquí están los nombres, ahora en mayúsculas y claramente separados por comas.
BRIAN, NATE, NEAL, RAJU, SARA, SCOTT
El
FUENTE: