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ícateQué 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.
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
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
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
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.