Usar alcance léxico y cierres
Muchos desarrolladores tienen este malentendido y creen que el uso de expresiones lambda generará redundancia de código y reducirá la calidad del código. Por el contrario, no importa cuán complejo sea el código, no comprometeremos la calidad del código en aras de la simplicidad, como veremos a continuación.
Pudimos reutilizar la expresión lambda en el ejemplo anterior; sin embargo, si coincidimos con otra letra, el problema de redundancia de código regresa rápidamente. Primero analicemos este problema más a fondo y luego usemos el alcance léxico y los cierres para resolverlo.
Redundancia causada por expresiones lambda
Filtremos esas letras que comienzan con N o B de amigos. Continuando con el ejemplo anterior, el código que escribimos podría verse así:
Copie el código de código de la siguiente manera:
Predicado final<Cadena> comienzaConN = nombre -> nombre.startsWith("N");
Predicado final<Cadena> comienzaConB = nombre -> nombre.startsWith("B");
cuenta larga finalFriendsStartN =
amigos.stream()
.filter(startsWithN).count();
cuenta larga finalFriendsStartB =
amigos.stream()
.filter(startsWithB).count();
El primer predicado determina si el nombre comienza con N y el segundo determina si el nombre comienza con B. Pasamos estas dos instancias a dos llamadas a métodos de filtro respectivamente. Esto parece razonable, pero los dos predicados son redundantes, son simplemente letras diferentes en el cheque. Veamos cómo podemos evitar esta redundancia.
Utilice el alcance léxico para evitar redundancias
En la primera solución, podemos extraer las letras como parámetros de la función y pasar esta función al método de filtro. Este es un buen método, pero no todas las funciones aceptan el filtro. Solo acepta funciones con un solo parámetro. Ese parámetro corresponde al elemento de la colección y devuelve un valor booleano. Espera que lo que se pasa sea un predicado.
Esperamos que haya un lugar donde esta letra pueda almacenarse en caché hasta que se pase el parámetro (en este caso, el parámetro de nombre). Creemos una nueva función como esta.
Copie el código de código de la siguiente manera:
Predicado estático público <Cadena> checkIfStartsWith (letra de cadena final) {
devolver nombre -> nombre.startsWith(letra);
}
Definimos una función estática checkIfStartsWith, que recibe un parámetro String y devuelve un objeto Predicate, que se puede pasar al método de filtro para su uso posterior. A diferencia de las funciones de orden superior que vimos anteriormente, que toman funciones como parámetros, este método devuelve una función. Pero también es una función de orden superior, que ya hemos mencionado en Evolución, no cambio, en la página 12.
El objeto Predicado devuelto por el método checkIfStartsWith es algo diferente de otras expresiones lambda. En la declaración nombre de retorno -> nombre.startsWith (letra), sabemos exactamente qué es el nombre, es el parámetro pasado a la expresión lambda. Pero ¿qué es exactamente la letra variable? Está fuera del dominio de la función anónima. Java encuentra el dominio donde se define la expresión lambda y descubre la letra variable. A esto se le llama alcance léxico. El alcance léxico es algo muy útil, nos permite almacenar en caché una variable en un alcance para usarla posteriormente en otro contexto. Debido a que esta expresión lambda usa variables en su alcance, esta situación también se denomina cierre. En cuanto a las restricciones de acceso al alcance léxico, ¿puedes leer las restricciones de alcance léxico en la página 31?
¿Existe alguna restricción en el alcance léxico?
En una expresión lambda, solo podemos acceder a tipos finales en su alcance o a variables locales de tipo final.
La expresión lambda se puede llamar inmediatamente, retrasada o desde un hilo diferente. Para evitar conflictos raciales, las variables locales en el dominio al que accedemos no pueden modificarse una vez inicializadas. Cualquier operación de modificación provocará una excepción de compilación.
Marcarlo como final resuelve este problema, pero Java no nos obliga a marcarlo de esta manera. De hecho, Java analiza dos cosas. Una es que la variable a la que se accede debe inicializarse en el método en el que está definida y antes de que se defina la expresión lambda. En segundo lugar, los valores de estas variables no se pueden modificar, es decir, en realidad son de tipo final, aunque no están marcados como tales.
Las expresiones lambda sin estado son constantes de tiempo de ejecución, mientras que aquellas que usan variables locales tienen una sobrecarga computacional adicional.
Al llamar al método de filtro, podemos usar la expresión lambda devuelta por el método checkIfStartsWith, así:
Copie el código de código de la siguiente manera:
cuenta larga finalFriendsStartN =
amigos.stream() .filter(checkIfStartsWith("N")).count();
cuenta larga finalFriendsStartB = amigos.stream()
.filter(checkIfStartsWith("B")).count();
Antes de llamar al método de filtro, primero llamamos al método checkIfStartsWith() y le pasamos las letras deseadas. Esta llamada devuelve rápidamente una expresión lambda, que luego pasamos al método de filtro.
Al crear una función de orden superior (checkIfStartsWith en este caso) y utilizar el alcance léxico, eliminamos con éxito la redundancia del código. Ya no necesitamos determinar repetidamente si el nombre comienza con una letra determinada.
Refactorizar, reducir el alcance
En el ejemplo anterior usamos un método estático, pero no queremos usarlo para almacenar en caché las variables, lo que arruinaría nuestro código. Es mejor limitar el alcance de esta función al lugar donde se utiliza. Podemos usar una interfaz de función para lograr esto.
Copie el código de código de la siguiente manera:
Función final<Cadena, Predicado<Cadena>> comienzaConLetra = (Letra de cadena) -> {
Predicado<Cadena> checkStarts = (Nombre de cadena) -> nombre.startsWith(letra);
devolver chequeComienzos; };
Esta expresión lambda reemplaza el método estático original. Puede colocarse en una función y definirse antes de que sea necesario. La variable startWithLetter se refiere a una Función cuyo parámetro de entrada es Cadena y cuyo parámetro de salida es Predicado.
En comparación con el método estático, esta versión es mucho más simple, pero podemos continuar refactorizándola para hacerla más concisa. Desde un punto de vista práctico, esta función es la misma que el método estático anterior: ambos reciben una Cadena y devuelven un Predicado. En lugar de declarar explícitamente un predicado, lo reemplazamos por completo con una expresión lambda.
Copie el código de código de la siguiente manera:
Función final<Cadena, Predicado<Cadena>> comienzaConLetra = (Letra de cadena) -> (Nombre de cadena) -> nombre.startsWith(letra);
Nos hemos deshecho del desorden, pero también podemos eliminar la declaración de tipo para hacerla más concisa, y el compilador de Java deducirá el tipo según el contexto. Echemos un vistazo a la versión mejorada.
Copie el código de código de la siguiente manera:
Función final<Cadena, Predicado<Cadena>> comienzaConLetra =
letra -> nombre -> nombre.startsWith(letra);
Se necesita algo de esfuerzo para adaptarse a esta sintaxis concisa. Si te ciega, busca primero en otra parte. Hemos completado la refactorización del código y ahora podemos usarlo para reemplazar el método checkIfStartsWith() original, así:
Copie el código de código de la siguiente manera:
cuenta larga finalFriendsStartN = amigos.stream()
.filter(startsWithLetter.apply("N")).count();
cuenta larga finalFriendsStartB = amigos.stream()
.filter(startsWithLetter.apply("B")).count();
En esta sección utilizamos funciones de orden superior. Vimos cómo crear funciones dentro de funciones si pasamos una función a otra función y cómo devolver una función a partir de una función. Todos estos ejemplos demuestran la simplicidad y reutilización que aportan las expresiones lambda.
En esta sección hemos utilizado completamente las funciones de Función y Predicado, pero echemos un vistazo a la diferencia entre ellas. El predicado acepta un parámetro de tipo T y devuelve un valor booleano para representar verdadero o falso de su condición de juicio correspondiente. Cuando necesitemos hacer juicios condicionales, podemos usar Predicateg para completarlo. Los métodos como filter que filtran elementos reciben Predicado como parámetro. Funciton representa una función cuyos parámetros de entrada son variables de tipo T y devuelve un resultado de tipo R. Es más general que Predicate, que sólo puede devolver valores booleanos. Siempre que la entrada se convierta en una salida, podemos usar la Función, por lo que es razonable que el mapa use la Función como parámetro.
Como puedes ver, seleccionar elementos de una colección es muy sencillo. A continuación presentaremos cómo seleccionar solo un elemento de la colección.