Expresiones Lambda en Java. Parte 3

Tercer y ultimo post sobre las expresiones lambda en Java. En este post veremos algo bastante particular añadido en Java 8 que son los method references o referencia a métodos (o métodos referenciados, como sea que se traduzca al español), lo cual nos ayuda a reducir(en algunos casos) la cantidad de código para definir una expresión lambda. Los method references o referencia a métodos  permite implementar la funcionalidad de un método abstracto (de una interfaz funcional, obvio) usando un método ya existente.

Existen 4 tipos de method reference. A continación se enlistan los 4 tipos con su sintaxis entre paréntesis:

  • Referencia a métodos estáticos (NombreDelaClase::nombreDelMetodoEstatico)
  • Referencia a métodos de instancia de un objeto en particular (variableQueReferenciaAlObjeto::nombreDelMetodoDeInstancia)
  • Referencia a métodos de instancia de un objeto arbitrario de un tipo en particular (NombreDelaClase::nombreDelMetodoDeInstancia)
  • Referencia a constructores (NombreDelaClase::new)

Referencia a métodos estáticos

Tomemos como ejemplo el siguiente código:

import java.util.List;

public class MethodReferenceExample1 {
  public static void main(String[] args) {

    // Creando lista
    List<String> distros = List.of("Ubuntu", "Fedora", "Debian", "Red Hat");

    // Procesando cada elemento de la lista
    distros.forEach(elem -> procesar(elem));
  }

  public static void procesar(String info) {
    System.out.println(info);
  }
}

Vemos que hay lista con los nombres de algunas distribuciones de Linux, y se hace uso del método forEach para imprimir en pantalla cada uno de los nombres en pantalla. El método forEach recibe un Consumer, que recordando lo que vimos en el post anterior, un Consumer es una interfaz funcional que en su método abstracto se recibe un parámetro pero no regresa nada, osea que “consume” el valor que se recibe sin retornar nada a cambio, y al ser una interfaz funcional pues es posible usar una expresión lambda, lo cual es lo que se hace en este caso. La expresión lambda del ejemplo lo que hace es llamar al método estático println que se encuentra en System.out para imprimir en pantalla el valor. Puesto que dicha expresión lambda lo único que hace es llamar al método estático System.out.println, es posible solo pasar como parámetro una referencia a dicho método estático, sin necesidad de mandar una expresión lambda normal. Si se cambia el código de ejemplo para mandar la referencia del método System.out.println quedaría de la siguiente manera:

import java.util.List;

public class MethodReferenceExample1 {
  public static void main(String[] args) {

    // Creando lista
    List<String> distros = List.of("Ubuntu", "Fedora", "Debian", "Red Hat");

    // Procesando cada elemento de la lista
    distros.forEach(MethodReferenceExample1::procesar);
  }

  public static void procesar(String info) {
    System.out.println(info);
  }
}

En el ejemplo anterior la expresión lambda que estaba dentro del forEach es reemplazada por MethodReferenceExample1::procesar, nótese el uso de los dos puntos dobles :: que nos ayudan a hacer referencia a un método en particular. Lo que MethodReferenceExample1::procesar quiere decir es: Toma como referencia al método procesar que se encuentra en la clase MethodReferenceExample1. El funcionamiento de este ejemplo es exactamente el mismo al primer ejemplo, este seria el resultado de una ejecución de ambos ejemplos:

Ubuntu
Fedora
Debian
Red Hat

Veamos un segundo ejemplo.

interface MiInterfaz {
  void hacerAlgo();
}

public class MethodReferenceExample2 {

  public static void main(String[] args) {

    // MiInterfaz miInterfaz = () -> System.out.println("HOLA!");
    MiInterfaz miInterfaz = MethodReferenceExample2::saludar;

    miInterfaz.hacerAlgo();
  }

  public static void saludar() {
    System.out.println("HOLA!");
  }
}

En el ejemplo anterior vemos que en la linea 9 (que esta comentada) se hace la definición de la interfaz MiInterfaz usando una expresión lambda y se guardar en una variable con nombre miInterfaz. También tenemos en la linea 10 la misma definición pero usando una referencia al método estático saludar, lo cual provocara que al invocar el método hacerAlgo en miInterfaz se ejecutara lo que este en el método estático saludar. Este seria el resultado de ejecutar el ejemplo:

HOLA!

Referencia a métodos de instancia de un objeto en particular

Tomemos como ejemplo el siguiente código el cual es bastante similar al ejemplo anterior:

interface MiInterfaz {
  void hacerAlgo();
}

public class MethodReferenceExample3 {

  public static void main(String[] args) {
    MethodReferenceExample3 methodReferenceExample3 = new MethodReferenceExample3();

    // MiInterfaz miInterfaz = () -> System.out.println("HOLA!");
    MiInterfaz miInterfaz = methodReferenceExample3::saludar;

    miInterfaz.hacerAlgo();
  }

  public void saludar() {
    System.out.println("HOLA!");
  }
}

En este caso, el método saludar ya no es estático, por lo que para poder usarlo (y poder referenciarlo) es necesario crear un objeto de tipo MethodReferenceExample3, en la linea 8 creamos dicho objeto. Después, en la linea 11 hacemos referencia al método saludar (se hace una referencia a un método de instancia de un objeto en particular), en la linea 10 que esta comentada se muestra como seria sin usar referencia a métodos. Al ejecutar el código el resultado es igual al ejemplo pasado:

HOLA!

Referencia a métodos de instancia de un objeto arbitrario de un tipo en particular

Para entender este tipo de referencia, lo que se usa es el nombre de una clase en cuestión y luego la referencia al método de instancia de la clase en cuestión. Por ejemplo, Object::toString es una referencia al método toString de la clase Object. Esto es muy similar al primer tipo de referencia de métodos que vimos en este post que fueron las referencias a métodos estáticos, solo que ahora no hacemos referencia a ningún método estático (toString no es estático), sino mas bien hacemos referencia a un método de instancia de la clase en cuestión (toString es un método de instancia de la clase Object.

A continuación veamos un ejemplo. Primero haremos uso de una clase llamada Perrito que tiene un método de instancia llamada comer la cual imprime en pantalla que el perro esta comiendo, y también sobrescribe el método toString:

public class Perrito {
  private String nombre;

  public Perrito(String nombre) {
    this.nombre = nombre;
  }

  public void comer() {
    System.out.println(nombre + " esta comiendo");
  }

  @Override
  public String toString() {
    return "El perro se llama " + nombre;
  }
}

Ahora haremos uso de esa clase para crear una lista de perros y hacerlos que coman:

import java.util.List;

public class MethodReferenceExample4 {

  public static void main(String[] args) {

    // Creando lista
    List<Perrito> perros = List.of(new Perrito("Firulais"), new Perrito("Spike"), new Perrito("Princesa"));

    // Haciendo que cada perro coma
    // perros.forEach(perro -> perro.comer());
    perros.forEach(Perrito::comer);
  }
}

Como se puede ver, en la linea 12 pasamos como referencia el método comer de la clase Perrito, lo cual provocará que por cada perro se imprima un mensaje de que dicho perro esta comiendo. La linea 11 que esta comentada muestra como seria hacer lo mismo pero usando una expresión lambda tradicional. El resultado de ejecutar el ejemplo seria esto:

Firulais esta comiendo
Spike esta comiendo
Princesa esta comiendo

Referencia a constructores

Y finalmente llegamos al ejemplo con constructores. Para este ejemplo usaremos una lista de String que representará a nombres de perros, y la idea es crear una lista de tipo Perrito en base a esa lista nombres de perros, osea que de una lista List<String> queremos crear una lista de List<Perrito>. El siguiente código hace precisamente eso:

import java.util.List;
import java.util.stream.Collectors;

public class MethodReferenceExample5 {

  public static void main(String[] args) {

    // Creando lista de nombre de perros
    List<String> nombresDePerros = List.of("Firulais", "Spike", "Princesa");

    // Creando a los perritos
    // List<Perrito> perritos = nombresDePerros.stream().map(nombre -> new Perrito(nombre)).collect(Collectors.toList());
    // perritos.forEach(perro -> System.out.println(perro));
    List<Perrito> perritos = nombresDePerros.stream().map(Perrito::new).collect(Collectors.toList());
    perritos.forEach(Perrito::toString);
  }
}

En la linea 9 creamos la lista con los nombres. En la linea 14 usamos Stream para crear la lista de tipo Perrito. Si no sabes que es Stream considera que es algo para procesar los elemento de un origen de datos, que en este caso nuestro origen de datos de la lista de nombres de perros, para finalmente colectar el resultado de lo procesado. En el método map se pasa una referencia al constructor de Perrito a través de Perrito::new (que por cierto, map espera como parámetro una interfaz funcional de tipo Function), y después usamos el método collect para pasar lo procesado a una lista. La linea 12 que esta comentada muestra como se haría lo mismo pero sin usar referencia al método constructor de Perrito. Y finalmente, se imprime lo que tiene la lista nueva creada de Perrito usando forEach y una referencia al método de instancia toString, la linea 13 muestra como seria hacer lo mismo pero sin usar una referencia al método de instancia toString y a su vez usando una expresión lambda normal. El resultado de ejecutar el código seria el siguiente:

El perro se llama Firulais
El perro se llama Spike
El perro se llama Princesa

Entradas relacionadas

Dejar un Comentario