Referencias de métodos en Java 8

Reducir el código repetitivo siempre ha sido una tarea popular para los desarrolladores de Java. Las referencias de métodos nos permiten acortar las expresiones lambda más abajo en declaraciones concisas y legibles.

Introducción

El [azúcar sintáctico] más dulce (https://en.wikipedia.org/wiki/Syntactic_sugar) agregado a Java hasta ahora son definitivamente [Expresiones lambda] (/ expresiones lambda-en-java/).

Java es un lenguaje prolijo y eso puede obstaculizar la productividad y la legibilidad. Reducir el código estándar y repetitivo siempre ha sido una tarea popular entre los desarrolladores de Java, y generalmente se busca un código limpio, legible y conciso.

Lambda Expressions eliminó la necesidad de escribir código repetitivo engorroso cuando se trata de algunas tareas comunes al permitir que los desarrolladores las llamen sin que pertenezcan a una clase y las pasen como si fueran objetos.

Estas expresiones han visto un mayor uso con la API de flujos de Java, y Spring's [WebFlux](https://docs.spring.io/spring/ docs/current/spring-framework-reference/web-reactive.html) marco para crear aplicaciones reactivas y dinámicas.

Otra característica realmente útil agregada a Java 8 son las referencias de métodos, que hacen que las expresiones Lambda sean mucho más concisas y simples, al invocar (hacer referencia) a los métodos usando un nombre de método cuando la expresión Lambda se habría usado simplemente para llamar a un método.

Referencias de métodos

Las referencias a métodos son esencialmente expresiones lambda abreviadas, que se utilizan para invocar métodos.

Constan de dos partes:

1
Class::method;

Y un ejemplo común sería imprimir los resultados de, por ejemplo, suscribirse a un servicio de publicación o Java Stream:

1
someCodeChain.subscribe(System.out::println);

Repasemos un ejemplo de código imperativo, que luego convertiremos en código funcional a través de Lambda Expressions y finalmente lo acortaremos a través de Method References.

Estaremos haciendo una clase simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Employee {
    private int id;
    private String name;
    private int wage;
    private String position;

    // Constructor, getters and setters

    @Override
    public String toString() {
        return "Name: " + name + ", Wage: " + wage + ", Position: " + position;
    }

    public int compareTo(Employee employee) {
        if (this.wage <= employee.wage) {
            return 1;
        } else {
            return -1;
        }
    }
}

Si formamos esta clase en una colección, como ArrayList, no podríamos ordenarla usando el método de utilidad .sort() ya que no implementa la interfaz Comparable.

Lo que podemos hacer es definir un nuevo Comparador para estos objetos mientras llamamos al método .sort():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Employee emp1 = new Employee(1, "David", 1200, "Developer");
Employee emp2 = new Employee(2, "Tim", 1500, "Developer");
Employee emp3 = new Employee(3, "Martha", 1300, "Developer");

ArrayList<Employee> employeeList = new ArrayList<>();
employeeList.add(emp1);
employeeList.add(emp2);
employeeList.add(emp3);

Collections.sort(employeeList, new Comparator<Employee>() {
    public int compare(Employee emp1, Employee emp2) {
        return emp1.compareTo(emp2);
    }
});

System.out.println(employeeList);

Ejecutar este código producirá:

1
[Name: Tim, Wage: 1500, Position: Developer, Name: Martha, Wage: 1300, Position: Developer, Name: David, Wage: 1200, Position: Developer]

Aquí, la clase anónima (Comparator) define los criterios de comparación. Podemos hacerlo mucho más simple y corto usando una Expresión Lambda:

1
Collections.sort(employeeList, (e1, e2) -> e1.compareTo(e2));

Ejecutar este fragmento de código producirá:

1
[Name: Tim, Wage: 1500, Position: Developer, Name: Martha, Wage: 1300, Position: Developer, Name: David, Wage: 1200, Position: Developer]

Por otra parte, dado que todo lo que estamos haciendo con esta expresión Lambda es llamar a un solo método, podemos hacer referencia solo a ese método:

1
Collections.sort(employeeList, Employee::compareTo);

Y esto también producirá:

1
[Name: Tim, Wage: 1500, Position: Developer, Name: Martha, Wage: 1300, Position: Developer, Name: David, Wage: 1200, Position: Developer]

Tipos de referencia de métodos

Las referencias de métodos se pueden usar en un par de escenarios diferentes:

  • Métodos estáticos: Clase::staticMethodName
  • Métodos de instancia de objetos particulares: object::instanceMethodName
  • Métodos de instancia de objetos arbitrarios: Class::methodName
  • Referencia del constructor: Clase::nueva

Repasemos todos estos tipos a través de algunos ejemplos simples.

Referencias de métodos estáticos

Puede hacer referencia a cualquier método estático de una clase simplemente llamando a su clase contenedora con el nombre del método.

Definamos una clase con un método ’estático’ y luego hagamos referencia a ella desde otra clase:

1
2
3
4
5
6
public class ClassA {
    public static void raiseToThePowerOfTwo(double num) {
        double result = Math.pow(num, 2);
        System.out.println(result);
    }
}

Y ahora, desde otra clase, usemos el método de utilidad static:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class ClassB {
    public static void main(String[] args) {
        List<Double> integerList = new ArrayList<>();
        integerList.add(new Double(5));
        integerList.add(new Double(2));
        integerList.add(new Double(6));
        integerList.add(new Double(1));
        integerList.add(new Double(8));
        integerList.add(new Double(9));

        integerList.forEach(ClassA::raiseToThePowerOfTwo);
    }
}

Ejecutar este fragmento de código producirá:

1
2
3
4
5
6
25.0
4.0
36.0
1.0
64.0
81.0

Hay muchas clases de Java que ofrecen métodos de utilidad estáticos que se pueden usar aquí. En nuestro ejemplo, hemos utilizado un método personalizado, aunque no muy útil en este caso.

Métodos de instancia de objetos particulares

Puede llamar a un método desde un objeto instanciado en particular haciendo referencia al método usando la variable de referencia del objeto.

Esto se ilustra con mayor frecuencia a través de un comparador personalizado. Usaremos la misma clase Employee de antes y la misma lista para resaltar la diferencia entre estos dos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Employee {
    private int id;
    private String name;
    private int wage;
    private String position;

    // Constructor, getters and setters

    @Override
    public String toString() {
        return "Name: " + name + ", Wage: " + wage + ", Position: " + position;
    }

    public int compareTo(Employee employee) {
        if (this.wage <= employee.wage) {
            return 1;
        } else {
            return -1;
        }
    }
}

Ahora, definamos un CustomComparator:

1
2
3
4
5
public class CustomComparator {
    public int compareEntities(Employee emp1, Employee emp2) {
        return emp1.compareTo(emp2);
    }
}

Y finalmente, completemos una lista y la ordenemos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Employee emp1 = new Employee(1, "David", 1200, "Developer");
Employee emp2 = new Employee(2, "Tim", 1500, "Developer");
Employee emp3 = new Employee(3, "Martha", 1300, "Developer");

ArrayList<Employee> employeeList = new ArrayList<>();
employeeList.add(emp1);
employeeList.add(emp2);
employeeList.add(emp3);

// Initializing our CustomComparator
CustomComparator customComparator = new CustomComparator();

// Instead of making a call to an arbitrary Employee
// we're now providing an instance and its method
Collections.sort(employeeList, customComparator::compareEntities);

System.out.println(employeeList);

Ejecutar este código también producirá:

1
[Name: Tim, Wage: 1500, Position: Developer, Name: Martha, Wage: 1300, Position: Developer, Name: David, Wage: 1200, Position: Developer]

La principal diferencia es que al agregar otra capa, a través de CustomComparator, podemos agregar más funcionalidad para comparar y quitarla de la clase misma. Una clase como Employee no debería estar cargada con una lógica de comparación compleja y esto da como resultado un código más limpio y legible.

Por otro lado, a veces no deseamos definir comparadores personalizados e introducir uno es simplemente demasiado complicado. En tales casos, llamaríamos a un método desde un objeto arbitrario de un tipo particular, como se muestra en la siguiente sección.

Métodos de instancia de objetos arbitrarios

Este ejemplo ya se mostró al comienzo del artículo cuando resumimos el enfoque imperativo en un enfoque funcional a través de Lambda Expressions.

Aunque, en buena medida, dado que este enfoque se usa muy a menudo, echemos un vistazo a otro ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
List<Integer> integerList = new ArrayList<>();
integerList.add(new Integer(5));
integerList.add(new Integer(2));
integerList.add(new Integer(6));
integerList.add(new Integer(1));
integerList.add(new Integer(8));
integerList.add(new Integer(9));

// Referencing the non-static compareTo method from the Integer class
Collections.sort(integerList, Integer::compareTo);

// Referencing static method
integerList.forEach(System.out::print);

Ejecutar este fragmento de código produciría:

1
125689

Si bien esto puede parecer que es lo mismo que una llamada a un método estático, no lo es. Esto es equivalente a llamar a la Expresión Lambda:

1
Collections.sort(integerList, (Integer a, Integer b) -> a.compareTo(b));

Aquí, la distinción es más obvia. Si tuviéramos que llamar a un método ’estático’, se vería así:

1
Collections.sort(integerList, (Integer a, Integer b) -> SomeClass.compare(a, b));

Constructores de referencia

Puede hacer referencia a un constructor de clase de la misma manera que haría referencia a un método ’estático'.

Podría usar una referencia a un constructor en lugar de la instanciación de clase clásica:

1
2
3
4
5
// Classic instantiation
Employee employee = new Employee();

// Constructor reference
Employee employee2 = Employe::new;

Según el contexto, si hay varios constructores presentes, se utilizará el adecuado si se hace referencia a él:

1
Stream<Employee> stream = names.stream().map(Employee::new);

Debido a una secuencia de nombres, si está presente un constructor Employee(String name), se utilizará.

Otra forma en que podría usar referencias de constructores es cuando quiera mapear una secuencia en una matriz, manteniendo el tipo particular. Si simplemente lo mapeara y luego llamara a toArray(), obtendría una matriz de Objects en lugar de su tipo particular.

Si lo intentamos, di:

1
Employee[] employeeArray = employeeList.toArray();

Por supuesto, recibiremos un error del compilador ya que .toArray() devuelve una matriz de Objects. Enviarlo tampoco ayudará:

1
Employee[] employeeArray = (Employee[]) employeeList.toArray();

Pero esta vez, será una excepción de tiempo de ejecución - ClassCastException.

Podemos evitar eso con:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Making a list of employees
List<String> employeeList = Arrays.asList("David", "Scott");

// Mapping a list to Employee objects and returning them as an array
Employee[] employeeArray = employeeList.stream().map(Employee::new).toArray(Employee[]::new);

// Iterating through the array and printing information
for (int i = 0; i < employeeArray.length; i++) {
    System.out.println(employeeArray[i].toString());
}

Y con eso, obtenemos la salida:

1
2
Name: David, Wage: 0, Position: null
Name: Scott, Wage: 0, Position: null

Conclusión

Las referencias a métodos son un tipo de expresiones lambda que se utilizan simplemente para hacer referencia a un método en su llamada. Con ellos, escribir código puede ser mucho más conciso y legible.

Lambda Expressions ha presentado a los desarrolladores de Java un enfoque más funcional en la programación que les permite evitar escribir código detallado para operaciones simples.

Licensed under CC BY-NC-SA 4.0