1. Interfaces

Para comprender qué son las funciones lambda, primero debe comprender qué son las interfaces. Entonces, recordemos los puntos principales.

Una interfaz es una variación del concepto de una clase. Una clase muy truncada, digamos. A diferencia de una clase, una interfaz no puede tener sus propias variables (excepto las estáticas). Tampoco puede crear objetos cuyo tipo sea una interfaz:

  • No puedes declarar variables de la clase.
  • No puedes crear objetos.

Ejemplo:

interface Runnable
{
   void run();
}
Ejemplo de una interfaz estándar

Usando una interfaz

Entonces, ¿por qué se necesita una interfaz? Las interfaces solo se usan junto con la herencia. La misma interfaz puede ser heredada por diferentes clases, o como también se dice, las clases implementan la interfaz .

Si una clase implementa una interfaz, debe implementar los métodos declarados pero no implementados por la interfaz. Ejemplo:

interface Runnable
{
   void run();
}

class Timer implements Runnable
{
   void run()
   {
      System.out.println(LocalTime.now());
   }
}

class Calendar implements Runnable
{
   void run()
   {
      var date = LocalDate.now();
      System.out.println("Today: " + date.getDayOfWeek());
   }
}

La Timerclase implementa la Runnableinterfaz, por lo que debe declarar dentro de sí misma todos los métodos que están en la Runnableinterfaz e implementarlos, es decir, escribir código en el cuerpo de un método. Lo mismo ocurre con la Calendarclase.

Pero ahora Runnablelas variables pueden almacenar referencias a objetos que implementan la Runnableinterfaz.

Ejemplo:

Código Nota
Timer timer = new Timer();
timer.run();

Runnable r1 = new Timer();
r1.run();

Runnable r2 = new Calendar();
r2.run();

El run()método de la Timerclase se llamará


El run()método de la Timerclase se llamará


El run()método de la Calendarclase se llamará

Siempre puede asignar una referencia de objeto a una variable de cualquier tipo, siempre que ese tipo sea una de las clases antepasadas del objeto. Para las clases Timery Calendar, existen dos tipos de este tipo: Objecty Runnable.

Si asigna una referencia de objeto a una Objectvariable, solo puede llamar a los métodos declarados en la Objectclase. Y si asigna una referencia de objeto a una Runnablevariable, puede llamar a los métodos del Runnabletipo.

Ejemplo 2:

ArrayList<Runnable> list = new ArrayList<Runnable>();
list.add (new Timer());
list.add (new Calendar());

for (Runnable element: list)
    element.run();

Este código funcionará, porque los objetos Timery Calendartienen métodos de ejecución que funcionan perfectamente bien. Entonces, llamarlos no es un problema. Si acabáramos de agregar un método run() a ambas clases, entonces no podríamos llamarlas de una manera tan simple.

Básicamente, la Runnableinterfaz solo se usa como un lugar para colocar el método de ejecución.



2. Clasificación

Pasemos a algo más práctico. Por ejemplo, echemos un vistazo a la clasificación de cadenas.

Para ordenar alfabéticamente una colección de cadenas, Java tiene un gran método llamadoCollections.sort(collection);

Este método estático ordena la colección pasada. Y en el proceso de clasificación, realiza comparaciones por pares de sus elementos para comprender si los elementos deben intercambiarse.

Durante la clasificación, estas comparaciones se realizan utilizando el compareTométodo (), que tienen todas las clases estándar: Integer, String, ...

El método compareTo() de la clase Integer compara los valores de dos números, mientras que el método compareTo() de la clase String analiza el orden alfabético de las cadenas.

Entonces, una colección de números se ordenará en orden ascendente, mientras que una colección de cadenas se ordenará alfabéticamente.

Algoritmos de clasificación alternativos

Pero, ¿y si queremos ordenar las cadenas no alfabéticamente, sino por su longitud? ¿Y si queremos ordenar los números en orden descendente? ¿Qué haces en este caso?

Para manejar tales situaciones, la Collectionsclase tiene otro sort()método que tiene dos parámetros:

Collections.sort(collection, comparator);

Donde comparador es un objeto especial que sabe cómo comparar objetos en una colección durante una operación de clasificación . El término comparador proviene de la palabra inglesa comparar , que a su vez deriva de compare , que significa “comparar”.

Entonces, ¿qué es este objeto especial?

Comparatorinterfaz

Bueno, todo es muy simple. El tipo del sort()segundo parámetro del método esComparator<T>

Donde T es un parámetro de tipo que indica el tipo de los elementos de la colección , y Comparatores una interfaz que tiene un solo métodoint compare(T obj1, T obj2);

En otras palabras, un objeto comparador es cualquier objeto de cualquier clase que implemente la interfaz Comparator. La interfaz de Comparator parece muy simple:

public interface Comparator<Type>
{
   public int compare(Type obj1, Type obj2);
}
Código para la interfaz Comparator

El compare()método compara los dos argumentos que se le pasan.

Si el método devuelve un número negativo, eso significa obj1 < obj2. Si el método devuelve un número positivo, eso significa obj1 > obj2. Si el método devuelve 0, eso significa obj1 == obj2.

Aquí hay un ejemplo de un objeto comparador que compara cadenas por su longitud:

public class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
}
codigo de la StringLengthComparatorclase

Para comparar longitudes de cadenas, simplemente reste una longitud de la otra.

El código completo para un programa que ordena cadenas por longitud se vería así:

public class Solution
{
   public static void main(String[] args)
   {
      ArrayList<String> list = new ArrayList<String>();
      Collections.addAll(list, "Hello", "how's", "life?");
      Collections.sort(list, new StringLengthComparator());
   }
}

class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
}
Ordenar cadenas por longitud


3. Azúcar sintáctico

¿Qué piensas, se puede escribir este código de forma más compacta? Básicamente, solo hay una línea que contiene información útil: obj1.length() - obj2.length();.

Pero el código no puede existir fuera de un método, por lo que tuvimos que agregar un compare()método y, para almacenar el método, tuvimos que agregar una nueva clase: StringLengthComparator. Y también necesitamos especificar los tipos de las variables... Todo parece estar correcto.

Pero hay formas de acortar este código. Tenemos algo de azúcar sintáctico para ti. ¡Dos cucharadas!

Clase interna anónima

Puede escribir el código del comparador dentro del main()método y el compilador hará el resto. Ejemplo:

public class Solution
{
    public static void main(String[] args)
    {
        ArrayList<String> list = new ArrayList<String>();
        Collections.addAll(list, "Hello", "how's", "life?");

        Comparator<String> comparator = new Comparator<String>()
        {
            public int compare (String obj1, String obj2)
            {
                return obj1.length() – obj2.length();
            }
        };

        Collections.sort(list, comparator);
    }
}
Ordenar cadenas por longitud

¡Puede crear un objeto que implemente la Comparatorinterfaz sin crear explícitamente una clase! El compilador lo creará automáticamente y le dará un nombre temporal. Comparemos:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
};
Clase interna anónima
Comparator<String> comparator = new StringLengthComparator();

class StringLengthComparator implements Comparator<String>
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
}
StringLengthComparatorclase

El mismo color se utiliza para indicar bloques de código idénticos en los dos casos diferentes. Las diferencias son bastante pequeñas en la práctica.

Cuando el compilador encuentra el primer bloque de código, simplemente genera un segundo bloque de código correspondiente y le da a la clase un nombre aleatorio.


4. Expresiones Lambda en Java

Digamos que decide usar una clase interna anónima en su código. En este caso, tendrás un bloque de código como este:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
};
Clase interna anónima

Aquí combinamos la declaración de una variable con la creación de una clase anónima. Pero hay una manera de acortar este código. Por ejemplo, así:

Comparator<String> comparator = (String obj1, String obj2) ->
{
    return obj1.length() – obj2.length();
};

El punto y coma es necesario porque aquí no solo tenemos una declaración de clase implícita, sino también la creación de una variable.

Una notación como esta se llama expresión lambda.

Si el compilador encuentra una notación como esta en su código, simplemente genera la versión detallada del código (con una clase interna anónima).

Tenga en cuenta que al escribir la expresión lambda, no solo omitimos el nombre de la clase, sino también el nombre del método.Comparator<String>int compare()

La compilación no tendrá problemas para determinar el método , porque una expresión lambda se puede escribir sólo para interfaces que tienen un solo método . Por cierto, hay una manera de eludir esta regla, pero aprenderá sobre eso cuando comience a estudiar OOP con mayor profundidad (estamos hablando de métodos predeterminados).

Veamos de nuevo la versión detallada del código, pero atenuaremos la parte que se puede omitir al escribir una expresión lambda:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
};
Clase interna anónima

Parece que no se omitió nada importante. De hecho, si la Comparatorinterfaz tiene solo un compare()método, el compilador puede recuperar por completo el código atenuado del código restante.

Clasificación

Por cierto, ahora podemos escribir el código de clasificación así:

Comparator<String> comparator = (String obj1, String obj2) ->
{
   return obj1.length() – obj2.length();
};
Collections.sort(list, comparator);

O incluso así:

Collections.sort(list, (String obj1, String obj2) ->
   {
      return obj1.length() – obj2.length();
   }
);

Simplemente reemplazamos inmediatamente la comparatorvariable con el valor que se asignó a la comparatorvariable.

Tipo de inferencia

Pero eso no es todo. El código de estos ejemplos se puede escribir de forma aún más compacta. Primero, el compilador puede determinar por sí mismo que las variables obj1y obj2son Strings. Y en segundo lugar, las llaves y la declaración de devolución también se pueden omitir si solo tiene un comando en el código del método.

La versión abreviada sería así:

Comparator<String> comparator = (obj1, obj2) ->
   obj1.length() – obj2.length();

Collections.sort(list, comparator);

Y si en lugar de usar la comparatorvariable, usamos inmediatamente su valor, entonces obtenemos la siguiente versión:

Collections.sort(list, (obj1, obj2) ->  obj1.length() — obj2.length() );

Bueno, ¿qué piensas de eso? Solo una línea de código sin información superflua, solo variables y código. ¡No hay manera de hacerlo más corto! ¿O hay?



5. Cómo funciona

De hecho, el código se puede escribir de forma aún más compacta. Pero más sobre eso más adelante.

Puede escribir una expresión lambda donde usaría un tipo de interfaz con un solo método.

Por ejemplo, en el código , puede escribir una expresión lambda porque la firma del método es así:Collections.sort(list, (obj1, obj2) -> obj1.length() - obj2.length());sort()

sort(Collection<T> colls, Comparator<T> comp)

Cuando pasamos la ArrayList<String>colección como primer argumento al método sort, el compilador pudo determinar que el tipo del segundo argumento es . Y a partir de esto, concluyó que esta interfaz tiene un solo método. Todo lo demás es un tecnicismo.Comparator<String>int compare(String obj1, String obj2)