Expresiones Lambda en Java

Las funciones Lambda han sido una adición que vino con Java 8 y fue el primer paso del lenguaje hacia la programación funcional, siguiendo una tendencia general hacia...

Introducción

Las funciones Lambda han sido una adición que vino con [Java 8] (https://www.oracle.com/technetwork/java/javase/overview/java8-2100321.html), y fue el primer paso del lenguaje hacia programación funcional, siguiendo una tendencia general hacia la implementación de características útiles de varios [paradigmas] compatibles (https://en.wikipedia.org/wiki/Programming_paradigm).

La motivación para introducir las funciones lambda fue principalmente reducir el engorroso código repetitivo que pasaba a lo largo de instancias de clase para simular funciones anónimas de otros lenguajes.

Aquí hay un ejemplo:

1
2
3
4
5
6
7
8
9
String[] arr = { "family", "illegibly", "acquired", "know", "perplexing", "do", "not", "doctors", "where", "handwriting", "I" };

Arrays.sort(arr, new Comparator<String>() {
    @Override public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

System.out.println(Arrays.toString(arr));

Como puede ver, todo el asunto de instanciar una nueva clase comparador y anular su contenido es un fragmento de código repetitivo del que podemos prescindir, ya que\ siempre es lo mismo.

Toda la línea Arrays.sort() se puede reemplazar por algo mucho más corto y agradable, pero funcionalmente equivalente:

1
Arrays.sort(arr, (s1,s2) -> s1.length() - s2.length());

Estos pequeños y agradables fragmentos de código que hacen lo mismo que sus homólogos detallados se denominan azúcar sintáctica. Esto se debe a que no agregan funcionalidad a un idioma, sino que lo hacen más compacto y legible. Las funciones Lambda son un ejemplo de azúcar sintáctico para Java.

Aunque sugiero leer este artículo en orden, si no está familiarizado con el tema, aquí hay una lista rápida de lo que cubriremos para una referencia más fácil:

Lambdas como objetos

Antes de entrar en el meollo de la sintaxis lambda en sí, deberíamos echar un vistazo a qué funciones lambda son en primer lugar y cómo se usan.

Como se mencionó, son simplemente azúcar sintáctico, pero son azúcar sintáctico específicamente para objetos que implementan una interfaz de método único.

En esos objetos, la implementación lambda se considera la implementación de dicho método. Si la lambda y la interfaz coinciden, la función lambda se puede asignar a una variable del tipo de esa interfaz.

Coincidencia de interfaz de método único

Para hacer coincidir una lambda con una interfaz de método único, también llamada "interfaz funcional", se deben cumplir varias condiciones:

  • La interfaz funcional tiene que tener exactamente un método no implementado, y ese método (naturalmente) tiene que ser abstracto. La interfaz puede contener métodos estáticos y predeterminados implementados dentro de ella, pero lo importante es que hay exactamente un método abstracto.
  • El método abstracto tiene que aceptar argumentos, en el mismo orden, que correspondan a los parámetros que acepta lambda.
  • El tipo de retorno tanto del método como de la función lambda debe coincidir.

Si se cumple todo eso, se han realizado todas las condiciones para la coincidencia y puede asignar su lambda a la variable.

Definamos nuestra interfaz:

1
2
3
public interface HelloWorld {
    abstract void world();
}

Como puedes ver, tenemos una interfaz funcional bastante inútil.

Contiene exactamente una función, y esa función puede hacer cualquier cosa, siempre que no acepte argumentos ni devuelva valores.

Vamos a hacer un programa Hello World simple usando esto, aunque la imaginación es el límite si quieres jugar con él:

1
2
3
4
5
6
public class Main {
    public static void main(String[] args) {
        HelloWorld hello = () -> System.out.println("Hello World!");
        hello.world();
    }
}

Como podemos ver, si ejecutamos esto, nuestra función lambda se ha emparejado con éxito con la interfaz HelloWorld, y ahora se puede usar el objeto hello para acceder a su método.

La idea detrás de esto es que puede usar lambdas donde quiera que use interfaces funcionales para pasar funciones. Si recuerda nuestro ejemplo de Comparator, Comparator<T> es en realidad una interfaz funcional, que implementa un único método: comparar().

Es por eso que podríamos reemplazarlo con una lambda que se comporte de manera similar a ese método.

Implementación

La idea básica detrás de las funciones lambda es la misma que la idea básica detrás de los métodos: toman parámetros y los usan dentro del cuerpo que consiste en expresiones.

La implementación es un poco diferente. Tomemos el ejemplo de nuestra lambda de clasificación String:

1
(s1,s2) -> s1.length() - s2.length()

Su sintaxis puede entenderse como:

1
parameters -> body

Parámetros

Parámetros son los mismos que los parámetros de función, esos son valores que se pasan a una función lambda para que haga algo con ellos.

Los parámetros suelen estar entre corchetes y separados por comas, aunque en el caso de una lambda, que recibe solo un parámetro, se pueden omitir los corchetes.

Una función lambda puede tomar cualquier cantidad de parámetros, incluido cero, por lo que podría tener algo como esto:

1
() -> System.out.println("Hello World!")

Esta función lambda, cuando se combina con una interfaz correspondiente, funcionará igual que la siguiente función:

1
2
3
static void printing(){
    System.out.println("Hello World!");
}

De manera similar, podemos tener funciones lambda con uno, dos o más parámetros.

Un ejemplo clásico de una función con un parámetro es trabajar en cada elemento de una colección en un bucle forEach:

1
2
3
4
5
6
public class Main {
    public static void main(String[] args) {
        LinkedList<Integer> childrenAges = new LinkedList<Integer>(Arrays.asList(2, 4, 5, 7));
        childrenAges.forEach( age -> System.out.println("One of the children is " + age + " years old."));
    }
}

Aquí, el único parámetro es edad. Tenga en cuenta que eliminamos los paréntesis aquí, porque eso está permitido cuando solo tenemos un parámetro.

El uso de más parámetros funciona de manera similar, solo están separados por una coma y encerrados entre paréntesis. Ya hemos visto lambda de dos parámetros cuando lo comparamos con Comparator para ordenar cadenas.

Cuerpo

El cuerpo de una expresión lambda consta de una sola expresión o un bloque de instrucciones.

Si especifica una sola expresión como el cuerpo de una función lambda (ya sea en un bloque de instrucciones o por sí misma), la lambda devolverá automáticamente la evaluación de esa expresión.

Si tiene varias líneas en su bloque de declaración, o si simplemente quiere (es un país libre), puede usar explícitamente una declaración de devolución dentro de un bloque de declaración:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// just the expression
(s1,s2) -> s1.length() - s2.length()

// statement block
(s1,s2) -> { s1.length() - s2.length(); }

// using return
(s1,s2) -> {
    s1.length() - s2.length();
    return; // because forEach expects void return
}

Puede intentar sustituir cualquiera de estos en nuestro ejemplo de clasificación al principio del artículo, y encontrará que todos funcionan exactamente igual.

Captura de variables {#captura de variables}

La captura de variables permite que las lambdas utilicen variables declaradas fuera de la propia lambda.

Hay tres tipos muy similares de captura de variables:

  • captura de variables locales
  • captura de variable de instancia
  • captura de variables estáticas

La sintaxis es casi idéntica a cómo accedería a estas variables desde cualquier otra función, pero las condiciones bajo las cuales puede hacerlo son diferentes.

Puede acceder a una variable local solo si es efectivamente final, lo que significa que no cambia su valor después de la asignación. No tiene que declararse explícitamente como final, pero es recomendable hacerlo para evitar confusiones. Si lo usa en una función lambda y luego cambia su valor, el compilador comenzará a quejarse.

La razón por la que no puede hacer esto es porque la lambda no puede hacer referencia de manera confiable a una variable local, ya que puede destruirse antes de ejecutar la lambda. Debido a esto, hace una copia profunda. Cambiar la variable local puede conducir a un comportamiento confuso, ya que el programador puede esperar que cambie el valor dentro de la lambda, por lo que para evitar confusiones, está explícitamente prohibido.

Cuando se trata de variables de instancia, si su lambda está dentro de la misma clase que la variable a la que está accediendo, simplemente puede usar this.field para acceder a un campo en esa clase. Además, el campo no tiene que ser definitivo, y se puede cambiar más adelante durante el transcurso del programa.

Esto se debe a que si una lambda se define dentro de una clase, se instancia junto con esa clase y se vincula a esa instancia de clase, y por lo tanto puede referirse fácilmente al valor del campo que necesita.

Las variables estáticas se capturan de manera muy similar a las variables de instancia, excepto por el hecho de que no usaría this para referirse a ellas. Se pueden cambiar y no es necesario que sean definitivos por las mismas razones.

Método de referencia

A veces, las lambdas son solo sustitutos de un método específico. En el espíritu de hacer que la sintaxis sea corta y dulce, en realidad no tiene que escribir toda la sintaxis cuando ese sea el caso. Por ejemplo:

1
s -> System.out.println(s)

es equivalente a:

1
System.out::println

La sintaxis :: le permitirá al compilador saber que solo desea una lambda que pase el argumento dado a println. Siempre debe anteponer el nombre del método con :: donde escribiría una función lambda; de lo contrario, accedería al método como lo haría normalmente, lo que significa que todavía tiene que especificar la clase propietaria antes de los dos puntos dobles.

Hay varios tipos de referencias de métodos, según el tipo de método que esté llamando:

  • referencia de método estático
  • referencia de método de parámetro
  • referencia de método de instancia
  • referencia del método constructor
Referencia de método estático

Necesitamos una interfaz:

1
2
3
public interface Average {
    abstract double average(double a, double b);
}

Una función estática:

1
2
3
4
5
public class LambdaFunctions {
    static double averageOfTwo(double a, double b){
        return (a+b)/2;
    }
}

Y nuestra función lambda y llamada en main:

1
2
Average avg = LambdaFunctions::averageOfTwo;
System.out.println(avg.average(20.3, 4.5));
Referencia de método de parámetro {#referencia de método de parámetro}

De nuevo, estamos escribiendo main.

1
2
3
Comparator<Double> cmp = Double::compareTo;
Double a = 20.3;
System.out.println(cmp.compare(a, 4.5));

El Double::compareTo lambda es equivalente a:

1
Comparator<Double> cmp = (a, b) -> a.compareTo(b)
Referencia de método de instancia {#referencia de método de instancia}

Si tomamos nuestra clase LambdaFunctions y nuestra función averageOfTwo (de Referencia de método estático) y las hacemos no estáticas, obtendremos lo siguiente:

1
2
3
4
5
public class LambdaFunctions {
    double averageOfTwo(double a, double b){
        return (a+b)/2;
    }
}

Para acceder a esto, ahora necesitamos una instancia de la clase, por lo que tendríamos que hacer esto en main:

1
2
3
LambdaFunctions lambda = new LambdaFunctions();
Average avg = lambda::averageOfTwo;
System.out.println(avg.average(20.3, 4.5));
Referencia del método constructor

Si tenemos una clase llamada MyClass y queremos llamar a su constructor a través de una función lambda, nuestra lambda se verá así:

1
MyClass::new

Aceptará tantos argumentos como pueda coincidir con uno de los constructores.

Conclusión

En conclusión, las lambdas son una característica útil para hacer que nuestro código sea más simple, más corto y más legible.

Algunas personas evitan usarlos cuando hay muchos Juniors en el equipo, por lo que le aconsejo consultar con su equipo antes de refactorizar todo su código, pero cuando todos están en la misma página, son un gran herramienta

Ver también {#ver también}

Aquí hay algunas lecturas adicionales sobre cómo y dónde aplicar las funciones lambda:

Licensed under CC BY-NC-SA 4.0