Java

Tutorial Java: Métodos referenciados

Aprende a usar referencias a métodos en Java 8 para escribir código más conciso y legible con ejemplos claros y consejos prácticos.

Aprende Java y certifícate

Qué son las referencias a métodos

Las referencias a métodos son una característica introducida en Java 8 que permite utilizar métodos existentes como implementaciones de interfaces funcionales. Esta funcionalidad representa una forma más concisa y legible de expresar ciertos tipos de expresiones lambda, especialmente cuando la expresión lambda simplemente invoca un método ya existente con los mismos parámetros.

En esencia, una referencia a método es una forma abreviada de escribir una expresión lambda que no hace nada más que llamar a un método específico. En lugar de definir explícitamente los parámetros y el cuerpo de la lambda, simplemente "apuntamos" al método que queremos utilizar mediante una sintaxis especial.

La sintaxis básica para una referencia a método utiliza el operador de doble dos puntos (::) y sigue este patrón:

NombreClase::nombreMetodo

Para entender mejor el concepto, veamos un ejemplo comparativo:

// Usando una expresión lambda tradicional
Consumer<String> printer1 = s -> System.out.println(s);

// Usando una referencia a método equivalente
Consumer<String> printer2 = System.out::println;

En este ejemplo, ambas expresiones hacen exactamente lo mismo: definen un Consumer<String> que imprime el string recibido. Sin embargo, la segunda forma es más concisa y comunica directamente la intención del código.

Las referencias a métodos funcionan gracias a la inferencia de tipos de Java. El compilador determina automáticamente qué versión del método debe utilizarse basándose en el contexto, específicamente en la interfaz funcional que se está implementando.

Veamos otro ejemplo práctico con una lista y el método forEach:

List<String> nombres = Arrays.asList("Ana", "Carlos", "Beatriz", "David");

// Usando lambda
nombres.forEach(nombre -> System.out.println(nombre));

// Usando referencia a método
nombres.forEach(System.out::println);

Las referencias a métodos son particularmente útiles cuando trabajamos con operaciones de streams, ya que permiten expresar transformaciones y operaciones de manera más clara:

List<String> nombres = Arrays.asList("ana", "carlos", "beatriz");

// Convertir cada nombre a mayúsculas usando lambda
List<String> mayusculas1 = nombres.stream()
                                 .map(s -> s.toUpperCase())
                                 .collect(Collectors.toList());

// Usando referencia a método
List<String> mayusculas2 = nombres.stream()
                                 .map(String::toUpperCase)
                                 .collect(Collectors.toList());

Las referencias a métodos no solo mejoran la legibilidad del código, sino que también hacen que sea más mantenible, ya que expresan directamente la intención sin código adicional. Además, en algunos casos pueden ofrecer pequeñas mejoras de rendimiento al evitar la creación de funciones lambda anónimas.

Es importante entender que las referencias a métodos no son una característica independiente, sino que están estrechamente relacionadas con las interfaces funcionales y las expresiones lambda. De hecho, podemos considerar las referencias a métodos como una forma especializada y simplificada de expresiones lambda para casos específicos donde solo se invoca un método.

Para utilizar referencias a métodos de manera efectiva, es necesario comprender bien el contexto funcional en el que se utilizan y asegurarse de que la firma del método referenciado sea compatible con la interfaz funcional que se está implementando.

// Ejemplo con Comparator
List<String> nombres = Arrays.asList("Ana", "Carlos", "Beatriz", "David");

// Usando lambda para ordenar por longitud
nombres.sort((s1, s2) -> Integer.compare(s1.length(), s2.length()));

// Ordenar alfabéticamente usando referencia a método
nombres.sort(String::compareToIgnoreCase);

En este último ejemplo, la referencia al método compareToIgnoreCase de la clase String se utiliza para implementar un Comparator<String> que ordena los strings alfabéticamente ignorando mayúsculas y minúsculas.

Sintaxis de referencias: estáticos, de instancia y constructores

En Java, las referencias a métodos se dividen en tres categorías principales según el tipo de método al que hacen referencia. Cada categoría tiene su propia sintaxis y comportamiento específico, aunque todas comparten el operador de doble dos puntos (::) como elemento distintivo.

Referencias a métodos estáticos

Las referencias a métodos estáticos son probablemente las más sencillas de entender. Apuntan a métodos que pertenecen a una clase y no a una instancia específica. Su sintaxis es:

NombreClase::metodoEstatico

Estas referencias son ideales cuando necesitamos utilizar funciones de utilidad que no dependen del estado de un objeto. Por ejemplo:

// Convertir strings a enteros usando una expresión lambda
Function<String, Integer> conversor1 = s -> Integer.parseInt(s);

// Equivalente usando referencia a método estático
Function<String, Integer> conversor2 = Integer::parseInt;

// Uso práctico en un stream
List<String> numerosTexto = Arrays.asList("10", "20", "30", "40");
List<Integer> numeros = numerosTexto.stream()
                                   .map(Integer::parseInt)
                                   .collect(Collectors.toList());

En este caso, Integer::parseInt es una referencia al método estático parseInt de la clase Integer. El compilador infiere que debe usar la versión que acepta un String y devuelve un Integer, basándose en el contexto de la interfaz funcional Function<String, Integer>.

Referencias a métodos de instancia

Las referencias a métodos de instancia son un poco más complejas porque pueden ser de dos tipos:

1. Referencias a métodos de instancia de un objeto particular

Cuando ya tenemos una instancia específica de un objeto y queremos referenciar uno de sus métodos:

instanciaObjeto::metodoInstancia

Por ejemplo:

// Creamos un formateador específico
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");

// Referencia a método de esa instancia específica
Function<LocalDate, String> formateador = formatter::format;

// Uso práctico
LocalDate hoy = LocalDate.now();
String fechaFormateada = formateador.apply(hoy);

En este caso, formatter::format referencia al método format de la instancia específica formatter que ya hemos creado.

2. Referencias a métodos de instancia de un tipo arbitrario

Cuando queremos referenciar un método de instancia, pero no de un objeto específico sino de cualquier objeto de un tipo determinado:

Tipo::metodoInstancia

Este tipo de referencia es particularmente interesante porque el primer parámetro de la lambda se convierte implícitamente en el objeto receptor del método. Por ejemplo:

// Lambda que llama a un método de instancia en su parámetro
Function<String, Integer> longitud1 = s -> s.length();

// Equivalente con referencia a método
Function<String, Integer> longitud2 = String::length;

// Uso con BiPredicate (dos parámetros)
BiPredicate<String, String> contiene1 = (s, prefijo) -> s.startsWith(prefijo);
BiPredicate<String, String> contiene2 = String::startsWith;

// Ejemplo práctico
List<String> nombres = Arrays.asList("Ana", "Carlos", "Beatriz");
List<Integer> longitudes = nombres.stream()
                                 .map(String::length)
                                 .collect(Collectors.toList());

En el caso de String::length, el compilador entiende que debe llamar al método length() sobre el objeto String que recibe como parámetro. Para String::startsWith, el primer parámetro se convierte en el objeto receptor, y el segundo parámetro se pasa al método startsWith.

Referencias a constructores

Las referencias a constructores permiten crear nuevas instancias de objetos de forma funcional. Su sintaxis es similar a la de los métodos estáticos, pero utilizando la palabra clave new:

NombreClase::new

El compilador selecciona automáticamente el constructor adecuado basándose en los parámetros esperados por la interfaz funcional. Por ejemplo:

// Supplier que crea un ArrayList vacío
Supplier<List<String>> creadorLista1 = () -> new ArrayList<>();
Supplier<List<String>> creadorLista2 = ArrayList::new;

// Function que crea un ArrayList con capacidad inicial
Function<Integer, List<String>> creadorListaCapacidad = ArrayList::new;

// Constructor con parámetros
BiFunction<String, Integer, Empleado> creadorEmpleado = Empleado::new;

// Uso práctico: convertir strings a objetos Persona
List<String> nombres = Arrays.asList("Ana", "Carlos", "Beatriz");
List<Persona> personas = nombres.stream()
                              .map(Persona::new)  // Llama a Persona(String nombre)
                              .collect(Collectors.toList());

En el último ejemplo, Persona::new hace referencia al constructor de Persona que acepta un String. Esto es especialmente útil para mapear datos de un tipo a instancias de objetos.

Compatibilidad con interfaces funcionales

Para que una referencia a método funcione correctamente, la firma del método referenciado debe ser compatible con la interfaz funcional que se está implementando:

// Método estático con dos parámetros
public static int sumar(int a, int b) {
    return a + b;
}

// Referencia compatible con BiFunction<Integer, Integer, Integer>
BiFunction<Integer, Integer, Integer> operacion = MiClase::sumar;

// Referencia compatible con IntBinaryOperator (más eficiente para primitivos)
IntBinaryOperator operacionOptimizada = MiClase::sumar;

Es importante elegir la interfaz funcional más apropiada para cada caso, especialmente cuando se trabaja con tipos primitivos, donde las interfaces especializadas como IntFunction o LongConsumer pueden ofrecer mejor rendimiento al evitar el boxing/unboxing.

La elección entre los diferentes tipos de referencias a métodos dependerá del contexto específico y de la naturaleza del método que queremos referenciar, pero todas ellas nos permiten escribir código más conciso y expresivo cuando trabajamos con programación funcional en Java.

Cuándo usar referencias vs. lambdas

Aunque las referencias a métodos y las expresiones lambda son intercambiables en muchos casos, existen situaciones donde una opción resulta más adecuada que la otra. Conocer cuándo utilizar cada enfoque te permitirá escribir código más limpio, mantenible y expresivo.

Prioriza referencias a métodos cuando:

1. La lambda simplemente llama a un método existente

Si tu expresión lambda no hace nada más que invocar un método con exactamente los mismos parámetros que recibe, una referencia a método es casi siempre la mejor opción:

// Menos óptimo
stream.map(s -> s.toUpperCase());

// Mejor opción
stream.map(String::toUpperCase);

2. Necesitas mejorar la legibilidad del código

Las referencias a métodos suelen comunicar la intención del código de manera más directa, especialmente cuando los nombres de los métodos son descriptivos:

// Lambda que oculta la intención detrás de la implementación
usuarios.stream()
    .filter(u -> u.estaActivo() && u.tienePermisos())
    .collect(Collectors.toList());

// Referencia que expresa claramente la intención
usuarios.stream()
    .filter(Usuario::esUsuarioValido)  // Método que encapsula la lógica
    .collect(Collectors.toList());

3. Trabajas con constructores para transformar datos

Cuando necesitas crear nuevas instancias a partir de datos, las referencias a constructores son particularmente elegantes:

// Con lambda
List<Empleado> empleados = nombres.stream()
    .map(nombre -> new Empleado(nombre))
    .collect(Collectors.toList());

// Con referencia a constructor
List<Empleado> empleados = nombres.stream()
    .map(Empleado::new)
    .collect(Collectors.toList());

4. Quieres reutilizar lógica existente

Si ya tienes métodos que implementan la lógica que necesitas, las referencias evitan duplicación de código:

// Duplicando lógica en la lambda
numeros.removeIf(n -> n % 2 == 0);

// Reutilizando un método existente
public static boolean esPar(int n) {
    return n % 2 == 0;
}
numeros.removeIf(MiClase::esPar);

Prefiere expresiones lambda cuando:

1. Necesitas lógica personalizada que no existe como método

Si necesitas implementar una lógica específica que no corresponde exactamente a un método existente, las lambdas son más apropiadas:

// Lógica específica que no justifica crear un método separado
empleados.sort((e1, e2) -> e1.getSalario().compareTo(e2.getSalario()));

2. La implementación es simple y corta

Para operaciones muy simples, una lambda concisa puede ser más legible que crear y referenciar un método separado:

// Lambda simple y directa
botón.addActionListener(e -> System.out.println("Botón pulsado"));

// Sería excesivo crear un método solo para esto

3. Necesitas acceder a variables del contexto

Las lambdas pueden capturar variables del ámbito donde se definen (siempre que sean efectivamente finales), mientras que las referencias a métodos no:

String prefijo = "Usuario: ";

// Lambda que captura la variable prefijo
usuarios.forEach(u -> System.out.println(prefijo + u.getNombre()));

// No es posible hacer esto con una referencia a método

4. Requieres transformar o adaptar parámetros

Cuando necesitas modificar los parámetros antes de pasarlos a un método, las lambdas ofrecen esta flexibilidad:

// Lambda que adapta los parámetros
map.forEach((k, v) -> System.out.println(k + ": " + v));

// No es posible directamente con una referencia a método

Consideraciones de rendimiento

En términos de rendimiento, las diferencias entre lambdas y referencias a métodos son generalmente mínimas:

// Benchmark simple
Runnable lambda = () -> System.out.println("Hola");
Runnable referencia = System.out::println;

// El rendimiento es prácticamente idéntico en la mayoría de casos

Sin embargo, hay algunos matices a considerar:

  • Las referencias a métodos pueden ser ligeramente más eficientes en algunos casos, ya que el compilador puede optimizarlas mejor.
  • Para métodos que se invocan con alta frecuencia en bucles críticos, las referencias pueden ofrecer una pequeña ventaja.
  • La diferencia de rendimiento rara vez es significativa como para basar tu decisión únicamente en este factor.

Ejemplos comparativos en contextos reales

Veamos algunos ejemplos prácticos que ilustran cuándo cada enfoque resulta más adecuado:

Procesamiento de colecciones:

// Escenario: Filtrar números pares de una lista

// Con lambda - apropiado si la lógica es simple
List<Integer> pares = numeros.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

// Con referencia - mejor si ya tenemos un método de utilidad
List<Integer> pares = numeros.stream()
    .filter(Utilidades::esPar)
    .collect(Collectors.toList());

Manejo de eventos:

// Escenario: Configurar un listener para un botón

// Con lambda - mejor para lógica simple específica de este botón
botonGuardar.addActionListener(e -> guardarDatos());

// Con referencia - mejor si la lógica del handler es reutilizable
botonGuardar.addActionListener(this::manejarGuardado);

Operaciones de mapeo complejas:

// Escenario: Transformar datos de un formato a otro

// Con lambda - mejor cuando hay transformación de parámetros
List<ResumenUsuario> resumenes = usuarios.stream()
    .map(u -> new ResumenUsuario(u.getNombre(), u.getEmail()))
    .collect(Collectors.toList());

// Con referencia - mejor si existe un constructor o método que coincida exactamente
List<ResumenUsuario> resumenes = usuarios.stream()
    .map(ResumenUsuario::desdeUsuario)  // Método estático que hace la conversión
    .collect(Collectors.toList());

Operaciones de ordenación:

// Escenario: Ordenar una lista de productos

// Con lambda - mejor para lógica de comparación personalizada
productos.sort((p1, p2) -> Double.compare(p1.getPrecio(), p2.getPrecio()));

// Con referencia - mejor si la comparación es estándar o ya está implementada
productos.sort(Comparator.comparing(Producto::getPrecio));

Equilibrio entre concisión y claridad

La elección entre lambdas y referencias a métodos debe equilibrar la concisión con la claridad:

// Muy conciso pero potencialmente críptico
usuarios.stream().map(Usuario::getId).filter(Objects::nonNull).forEach(System.out::println);

// Más verboso pero más claro
usuarios.stream()
    .map(Usuario::getId)          // Obtener IDs
    .filter(Objects::nonNull)     // Filtrar IDs nulos
    .forEach(System.out::println); // Imprimir cada ID

En general, la decisión debe basarse en qué enfoque comunica mejor la intención del código para los desarrolladores que lo leerán en el futuro, incluyéndote a ti mismo. Si la referencia a método expresa claramente lo que estás tratando de hacer, suele ser la mejor opción. Si necesitas lógica personalizada o adaptación de parámetros, las lambdas ofrecen la flexibilidad necesaria.

La programación funcional en Java se beneficia de ambos enfoques, y los desarrolladores más efectivos saben cuándo aplicar cada uno para crear código que sea tanto conciso como comprensible.

Aprende Java online

Otros ejercicios de programación de Java

Evalúa tus conocimientos de esta lección Métodos referenciados con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Streams: match

Test

Gestión de errores y excepciones

Código

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Clases abstractas

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

API java.nio 2

Puzzle

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Interfaces

Código

Enumeraciones Enums

Código

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

CRUD de productos en Java

Proyecto

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Herencia

Código

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Mapas

Código

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

Todas las lecciones de Java

Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Instalación De Java

Introducción Y Entorno

Configuración De Entorno Java

Introducción Y Entorno

Tipos De Datos

Sintaxis

Variables

Sintaxis

Operadores

Sintaxis

Estructuras De Control

Sintaxis

Funciones

Sintaxis

Recursión

Sintaxis

Arrays Y Matrices

Sintaxis

Excepciones

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Encapsulación

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Sobrecarga De Métodos

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

La Clase Scanner

Programación Orientada A Objetos

Métodos De La Clase String

Programación Orientada A Objetos

Excepciones

Programación Orientada A Objetos

Records

Programación Orientada A Objetos

Pattern Matching

Programación Orientada A Objetos

Inferencia De Tipos Con Var

Programación Orientada A Objetos

Enumeraciones Enums

Programación Orientada A Objetos

Generics

Programación Orientada A Objetos

Clases Sealed

Programación Orientada A Objetos

Listas

Framework Collections

Conjuntos

Framework Collections

Mapas

Framework Collections

Funciones Lambda

Programación Funcional

Interfaz Funcional Consumer

Programación Funcional

Interfaz Funcional Predicate

Programación Funcional

Interfaz Funcional Supplier

Programación Funcional

Interfaz Funcional Function

Programación Funcional

Métodos Referenciados

Programación Funcional

Creación De Streams

Programación Funcional

Operaciones Intermedias Con Streams: Map()

Programación Funcional

Operaciones Intermedias Con Streams: Filter()

Programación Funcional

Operaciones Intermedias Con Streams: Distinct()

Programación Funcional

Operaciones Finales Con Streams: Collect()

Programación Funcional

Operaciones Finales Con Streams: Min Max

Programación Funcional

Operaciones Intermedias Con Streams: Flatmap()

Programación Funcional

Operaciones Intermedias Con Streams: Sorted()

Programación Funcional

Operaciones Finales Con Streams: Reduce()

Programación Funcional

Operaciones Finales Con Streams: Foreach()

Programación Funcional

Operaciones Finales Con Streams: Count()

Programación Funcional

Operaciones Finales Con Streams: Match

Programación Funcional

Api Optional

Programación Funcional

Transformación

Programación Funcional

Reducción Y Acumulación

Programación Funcional

Mapeo

Programación Funcional

Streams Paralelos

Programación Funcional

Agrupación Y Partición

Programación Funcional

Filtrado Y Búsqueda

Programación Funcional

Api Java.nio 2

Entrada Y Salida Io

Fundamentos De Io

Entrada Y Salida Io

Leer Y Escribir Archivos

Entrada Y Salida Io

Httpclient Moderno

Entrada Y Salida Io

Clases De Nio2

Entrada Y Salida Io

Api Java.time

Api Java.time

Localtime

Api Java.time

Localdatetime

Api Java.time

Localdate

Api Java.time

Executorservice

Concurrencia

Virtual Threads (Project Loom)

Concurrencia

Future Y Completablefuture

Concurrencia

Spring Framework

Frameworks Para Java

Micronaut

Frameworks Para Java

Maven

Frameworks Para Java

Gradle

Frameworks Para Java

Lombok Para Java

Frameworks Para Java

Quarkus

Frameworks Para Java

Ecosistema Jakarta Ee De Java

Frameworks Para Java

Introducción A Junit 5

Testing

Accede GRATIS a Java y certifícate

Certificados de superación de Java

Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender qué son las referencias a métodos y su sintaxis básica.
  • Diferenciar entre referencias a métodos estáticos, de instancia y a constructores.
  • Saber cuándo utilizar referencias a métodos en lugar de expresiones lambda.
  • Aplicar referencias a métodos en operaciones con streams y colecciones.
  • Evaluar consideraciones de rendimiento y legibilidad entre lambdas y referencias a métodos.