Comprendiendo las Referencias de Método en Java

Introducción

Las referencias de método en Java son una característica poderosa introducida en Java 8 que simplifica el uso de expresiones lambda. Permiten una manera más concisa y legible de referenciar métodos existentes, evitando la redundancia que puede conllevar el uso de clases anónimas. En este artículo, exploraremos en profundidad qué son las referencias de método, cómo se utilizan y su aplicación práctica en la programación Java.

1. Visión General

Uno de los cambios más bienvenidos en Java 8 fue la introducción de expresiones lambda, que nos permiten prescindir de las clases anónimas, reduciendo significativamente el código boilerplate y mejorando la legibilidad. Dentro de este contexto, las referencias de método son un tipo especial de expresión lambda que hace referencia a métodos existentes en lugar de escribir la implementación completa.

Las referencias de método pueden clasificarse en cuatro tipos:

  • Métodos estáticos
  • Métodos de instancia de objetos específicos
  • Métodos de instancia de un objeto arbitrario de un tipo particular
  • Constructores

En este tutorial, vamos a explorar cada uno de estos tipos de referencias de método en Java.

2. Referencia a un Método Estático

Comenzaremos con un ejemplo sencillo, capitalizando y imprimiendo una lista de Strings:

List<String> messages = Arrays.asList("hello", "baeldung", "readers!");

Podemos lograr este objetivo utilizando una expresión lambda que llama al método StringUtils.capitalize() directamente:

messages.forEach(word -> StringUtils.capitalize(word));

Sin embargo, también podemos utilizar una referencia de método para hacer referencia al método estático capitalize:

messages.forEach(StringUtils::capitalize);

Nota: Las referencias de método siempre utilizan el operador ::.

3. Referencia a un Método de Instancia de un Objeto Específico

Para demostrar este tipo de referencia de método, consideremos dos clases:

public class Bicycle {
private String brand;
private Integer frameSize;
// constructor estándar, getters y setters
}
public class BicycleComparator implements Comparator<Bicycle> {
@Override
public int compare(Bicycle a, Bicycle b) {
return a.getFrameSize().compareTo(b.getFrameSize());
}
}

Ahora, crearemos un objeto BicycleComparator para comparar los tamaños de los cuadros de bicicleta:

BicycleComparator bikeFrameSizeComparator = new BicycleComparator();

Podríamos utilizar una expresión lambda para ordenar las bicicletas por tamaño de cuadro, pero tendríamos que especificar dos bicicletas para la comparación:

createBicyclesList().stream()
.sorted((a, b) -> bikeFrameSizeComparator.compare(a, b));

En su lugar, podemos usar una referencia de método, lo que nos permite que el compilador maneje el paso de parámetros por nosotros:

createBicyclesList().stream()
.sorted(bikeFrameSizeComparator::compare);

La referencia del método es mucho más limpia y legible, ya que nuestra intención queda claramente reflejada en el código.

4. Referencia a un Método de Instancia de un Objeto Arbitrario de un Tipo Particular

Este tipo de referencia de método es similar al ejemplo anterior, pero sin necesidad de crear un objeto personalizado para realizar la comparación.

Creamos una lista de Integer que queremos ordenar:

List<Integer> numbers = Arrays.asList(5, 3, 50, 24, 40, 2, 9, 18);

Si usamos una expresión lambda clásica, ambos parámetros deben pasarse explícitamente, mientras que utilizar una referencia de método es mucho más directo:

numbers.stream()
.sorted((a, b) -> a.compareTo(b));
numbers.stream()
.sorted(Integer::compareTo);

Aunque sigue siendo una sola línea, la referencia de método es mucho más fácil de leer y entender.

5. Referencia a un Constructor

Podemos hacer referencia a un constructor de la misma manera que hicimos referencia a un método estático en nuestro primer ejemplo. La única diferencia es que utilizaremos la palabra new.

Creamos una lista de Bicycle a partir de una lista de String con diferentes marcas:

List<String> bikeBrands = Arrays.asList("Giant", "Scott", "Trek", "GT");

Primero, añadimos un nuevo constructor a nuestra clase Bicycle:

public Bicycle(String brand) {
this.brand = brand;
this.frameSize = 0;
}

A continuación, utilizamos nuestro nuevo constructor desde una referencia de método y hacemos un array de Bicycle a partir de la lista original de String:

bikeBrands.stream()
.map(Bicycle::new)
.toArray(Bicycle[]::new);

Observa cómo llamamos a los constructores de Bicycle y de Array utilizando una referencia de método, lo que proporciona a nuestro código una apariencia mucho más concisa y clara.

6. Ejemplos Adicionales y Limitaciones

Como hemos visto hasta ahora, las referencias de método son una forma excelente de hacer que nuestro código y nuestras intenciones sean muy claras y legibles. Sin embargo, no podemos utilizarlas para reemplazar todos los tipos de expresiones lambda, ya que tienen algunas limitaciones.

Su principal limitación es resultado de lo que también es su mayor fortaleza: la salida de la expresión anterior debe coincidir con los parámetros de entrada de la firma del método referenciado.

Por ejemplo:

createBicyclesList().forEach(b -> System.out.printf(
"La marca de la bicicleta es '%s' y su tamaño de cuadro es '%d'%n",
b.getBrand(),
b.getFrameSize()));

Este caso sencillo no puede expresarse con una referencia de método, porque el método printf requiere 3 parámetros en nuestro caso, y utilizar createBicyclesList().forEach() solo permitiría al método de referencia inferir un parámetro (el objeto Bicycle).

Finalmente, exploremos cómo crear una función de no operación que puede ser referenciada desde una expresión lambda. En este caso, queremos utilizar una expresión lambda sin usar sus parámetros.

Primero, creamos el método doNothingAtAll:

private static <T> void doNothingAtAll(Object... o) {
}

Como es un método varargs, funcionará en cualquier expresión lambda, sin importar el objeto referenciado o el número de parámetros inferidos.

Veamos cómo funciona en acción:

createBicyclesList()
.forEach((o) -> MethodReferenceExamples.doNothingAtAll(o));

7. Conclusión

En este tutorial, hemos aprendido qué son las referencias de método en Java y cómo utilizarlas para reemplazar expresiones lambda, mejorando así la legibilidad y clarificando la intención del programador. Las referencias de método no solo son más compuestas, sino que también aportan a la claridad del código, permitiendo que otros programadores (o el mismo en el futuro) entiendan más fácilmente las intenciones detrás de las operaciones realizadas.

Algunas recomendaciones finales para maximizar el uso de referencias de método en tu programación Java son:

  • Usa referencias de métodos siempre que sea posible para escribir código más limpio y legible.
  • Conoce las limitaciones y asegúrate de que los tipos coincidan, evitando situaciones en las que no puedas utilizar referencias de métodos.
  • Combina referencias de métodos con otros elementos de Java 8, como streams, para obtener el máximo rendimiento en tus aplicaciones.

Siguiendo estos consejos, podrás implementar referencias de método de manera efectiva y eficiente en tus proyectos Java.