Saltar a contenido

🔗 Referencias de métodos en Java

Las referencias de métodos son una forma más corta y elegante de escribir algunas expresiones lambda.

Dicho de forma muy simple:

Una referencia de método sirve para decirle a Java:
“usa este método que ya existe”.


🧠 Antes de empezar: recordatorio de lambdas

Una expresión lambda permite escribir una pequeña acción de forma rápida.

Por ejemplo:

List<String> nombres = List.of("Ana", "Luis", "Marta");

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

La parte importante es esta:

nombre -> System.out.println(nombre)

Esto significa:

Recibe un nombre y pásaselo al método println.

Pero fíjate bien: la lambda no está haciendo nada especial. Solo llama a un método que ya existe.

Por eso Java permite escribirlo de una forma más corta:

nombres.forEach(System.out::println);

✨ ¿Qué es una referencia de método?

Una referencia de método es una forma abreviada de escribir una lambda cuando la lambda solo llama a un método existente.

La sintaxis usa ::.

algo::metodo

El símbolo :: se puede leer como:

referencia al método

Por ejemplo:

System.out::println

Se puede leer como:

usa el método println del objeto System.out.


🧩 Lambda vs referencia de método

Estas dos instrucciones hacen lo mismo:

nombres.forEach(nombre -> System.out.println(nombre));
nombres.forEach(System.out::println);

La primera usa una lambda.

La segunda usa una referencia de método.

Java entiende que forEach necesita algo que reciba un elemento y haga algo con él.

Por eso interpreta esto:

System.out::println

como si fuera esto:

nombre -> System.out.println(nombre)

🧱 Idea clave

Una referencia de método no ejecuta el método en ese mismo instante.

Solo le entrega a Java una referencia para que lo ejecute cuando toque.

Ejemplo:

nombres.forEach(System.out::println);

Aquí println se ejecutará una vez por cada elemento de la lista.

Es como decirle a Java:

Cuando tengas cada nombre, pásaselo a println.


⚠️ Las referencias de métodos necesitan una interfaz funcional

Igual que las lambdas, las referencias de métodos solo funcionan cuando Java espera una interfaz funcional.

Una interfaz funcional es una interfaz que tiene un único método abstracto.

Algunas interfaces funcionales importantes son:

Interfaz funcional Método principal Significado sencillo
Consumer<T> accept(T t) Recibe un dato y no devuelve nada
Function<T, R> apply(T t) Recibe un dato y devuelve otro
Predicate<T> test(T t) Recibe un dato y devuelve true o false
Supplier<T> get() No recibe nada y devuelve un dato
Comparator<T> compare(T a, T b) Compara dos objetos

Por ejemplo, forEach espera un Consumer.

nombres.forEach(System.out::println);

System.out::println encaja porque puede recibir un dato y no devuelve nada.


🧪 Primer ejemplo completo

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> nombres = List.of("Ana", "Luis", "Marta");

        nombres.forEach(System.out::println);
    }
}

Salida:

Ana
Luis
Marta

Esto sería equivalente a:

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

📚 Tipos de referencias de métodos

En Java hay cuatro tipos principales:

  1. Referencia a un método estático.
  2. Referencia a un método de un objeto concreto.
  3. Referencia a un método de instancia de un objeto que llega como parámetro.
  4. Referencia a un constructor.

Vamos uno por uno.


1️⃣ Referencia a un método estático

📌 Sintaxis

Clase::metodoEstatico

Se usa cuando queremos llamar a un método static de una clase.


🧪 Ejemplo con Integer.parseInt

Supongamos que tenemos una interfaz funcional:

import java.util.function.Function;

Function<String, Integer> conversor;

Function<String, Integer> significa:

Recibe un String y devuelve un Integer.

Con lambda:

Function<String, Integer> conversor = texto -> Integer.parseInt(texto);

Con referencia de método:

Function<String, Integer> conversor = Integer::parseInt;

Ejemplo completo:

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> conversor = Integer::parseInt;

        int numero = conversor.apply("25");

        System.out.println(numero + 5);
    }
}

Salida:

30

La referencia:

Integer::parseInt

equivale a:

texto -> Integer.parseInt(texto)

🧪 Ejemplo con método estático propio

import java.util.function.Function;

public class Main {

    public static int calcularDoble(int numero) {
        return numero * 2;
    }

    public static void main(String[] args) {
        Function<Integer, Integer> operacion = Main::calcularDoble;

        System.out.println(operacion.apply(6));
    }
}

Salida:

12

Esto:

Main::calcularDoble

equivale a:

numero -> Main.calcularDoble(numero)

🧠 ¿Por qué funciona?

Porque el método calcularDoble recibe un int y devuelve un int.

Y Function<Integer, Integer> necesita exactamente algo parecido:

Integer -> Integer

Por eso Java dice:

Vale, este método encaja con esta interfaz funcional.


2️⃣ Referencia a un método de un objeto concreto

📌 Sintaxis

objeto::metodo

Se usa cuando ya tenemos un objeto creado y queremos usar uno de sus métodos.


🧪 Ejemplo con un objeto propio

import java.util.List;

class Mensajero {
    public void mostrarMensaje(String mensaje) {
        System.out.println("📢 Mensaje: " + mensaje);
    }
}

public class Main {
    public static void main(String[] args) {
        List<String> avisos = List.of("Revisar tarea", "Subir proyecto", "Estudiar lambdas");

        Mensajero mensajero = new Mensajero();

        avisos.forEach(mensajero::mostrarMensaje);
    }
}

Salida:

📢 Mensaje: Revisar tarea
📢 Mensaje: Subir proyecto
📢 Mensaje: Estudiar lambdas

Esto:

avisos.forEach(mensajero::mostrarMensaje);

equivale a:

avisos.forEach(aviso -> mensajero.mostrarMensaje(aviso));

🧠 Idea importante

Aquí el objeto ya existe:

Mensajero mensajero = new Mensajero();

La referencia de método no crea un Mensajero nuevo.

Solo dice:

Usa el método mostrarMensaje de este objeto concreto.


🧪 Otro ejemplo

import java.util.function.Consumer;

class Registro {
    public void guardar(String texto) {
        System.out.println("Guardando en registro: " + texto);
    }
}

public class Main {
    public static void main(String[] args) {
        Registro registro = new Registro();

        Consumer<String> accion = registro::guardar;

        accion.accept("Usuario creado");
        accion.accept("Producto eliminado");
    }
}

Salida:

Guardando en registro: Usuario creado
Guardando en registro: Producto eliminado

3️⃣ Referencia a un método de instancia de un objeto recibido como parámetro

Este tipo suele ser el más difícil al principio, así que vamos despacio.

📌 Sintaxis

Clase::metodoDeInstancia

Ejemplo:

String::toUpperCase

Cuidado: toUpperCase no es estático.

No podemos hacer esto:

String.toUpperCase(); // ❌ Incorrecto

Entonces, ¿por qué sí podemos escribir esto?

String::toUpperCase

Porque Java entiende que el objeto sobre el que se llamará el método será el dato que llegue como parámetro.


🧪 Ejemplo con String::toUpperCase

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, String> convertir = String::toUpperCase;

        System.out.println(convertir.apply("hola"));
    }
}

Salida:

HOLA

Esto:

String::toUpperCase

equivale a:

texto -> texto.toUpperCase()

🧪 Ejemplo con String::length

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> calcularLongitud = String::length;

        System.out.println(calcularLongitud.apply("Java"));
        System.out.println(calcularLongitud.apply("Programación"));
    }
}

Salida:

4
12

Esto:

String::length

equivale a:

texto -> texto.length()

🧪 Ejemplo con Predicate

Predicate<T> recibe un dato y devuelve true o false.

import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<String> estaVacio = String::isEmpty;

        System.out.println(estaVacio.test(""));
        System.out.println(estaVacio.test("Java"));
    }
}

Salida:

true
false

Esto:

String::isEmpty

equivale a:

texto -> texto.isEmpty()

🧠 Diferencia importante

No es lo mismo esto:

objeto::metodo

que esto:

Clase::metodo

En objeto::metodo, el objeto ya existe:

mensajero::mostrarMensaje

Equivale a:

texto -> mensajero.mostrarMensaje(texto)

En Clase::metodo, el objeto llega como parámetro:

String::toUpperCase

Equivale a:

texto -> texto.toUpperCase()

4️⃣ Referencia a un constructor

📌 Sintaxis

Clase::new

Se usa cuando queremos crear objetos usando un constructor.


🧪 Ejemplo sencillo con Supplier

Supplier<T> no recibe nada y devuelve un objeto.

Supongamos esta clase:

class Caja {
    public Caja() {
        System.out.println("Caja creada");
    }
}

Con lambda:

Supplier<Caja> creador = () -> new Caja();

Con referencia a constructor:

Supplier<Caja> creador = Caja::new;

Ejemplo completo:

import java.util.function.Supplier;

class Caja {
    public Caja() {
        System.out.println("Caja creada");
    }
}

public class Main {
    public static void main(String[] args) {
        Supplier<Caja> creador = Caja::new;

        Caja c1 = creador.get();
        Caja c2 = creador.get();
    }
}

Salida:

Caja creada
Caja creada

🧠 Ojo: Caja::new no crea la caja inmediatamente

Esto:

Supplier<Caja> creador = Caja::new;

no crea todavía una caja.

La caja se crea cuando llamamos a:

creador.get();

🧪 Constructor con parámetro

import java.util.function.Function;

class Persona {
    private String nombre;

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

    @Override
    public String toString() {
        return nombre;
    }
}

public class Main {
    public static void main(String[] args) {
        Function<String, Persona> crearPersona = Persona::new;

        Persona p1 = crearPersona.apply("Ana");
        Persona p2 = crearPersona.apply("Luis");

        System.out.println(p1);
        System.out.println(p2);
    }
}

Salida:

Ana
Luis

Esto:

Persona::new

equivale a:

nombre -> new Persona(nombre)

🧮 Referencias de métodos con Comparator

Las referencias de métodos se usan muchísimo para ordenar objetos.

Supongamos esta clase:

class Producto {
    private String nombre;
    private double precio;
    private int stock;

    public Producto(String nombre, double precio, int stock) {
        this.nombre = nombre;
        this.precio = precio;
        this.stock = stock;
    }

    public String getNombre() {
        return nombre;
    }

    public double getPrecio() {
        return precio;
    }

    public int getStock() {
        return stock;
    }

    @Override
    public String toString() {
        return nombre + " - " + precio + " € - stock: " + stock;
    }
}

📌 Ordenar por nombre

Con lambda:

productos.sort((p1, p2) -> p1.getNombre().compareTo(p2.getNombre()));

Con Comparator.comparing y referencia de método:

productos.sort(Comparator.comparing(Producto::getNombre));

Esto se lee así:

Ordena los productos usando el nombre de cada producto.


📌 Ordenar por precio

productos.sort(Comparator.comparingDouble(Producto::getPrecio));

Esto se lee así:

Ordena los productos usando el precio de cada producto.


📌 Ordenar por stock

productos.sort(Comparator.comparingInt(Producto::getStock));

Esto se lee así:

Ordena los productos usando el stock de cada producto.


📌 Ordenar por varios criterios

productos.sort(
        Comparator.comparing(Producto::getNombre)
                .thenComparingDouble(Producto::getPrecio)
                .thenComparingInt(Producto::getStock)
);

Esto significa:

  1. Ordena primero por nombre.
  2. Si dos productos tienen el mismo nombre, ordena por precio.
  3. Si también tienen el mismo precio, ordena por stock.

🧪 Ejemplo completo con Comparator

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

class Producto {
    private String nombre;
    private double precio;
    private int stock;

    public Producto(String nombre, double precio, int stock) {
        this.nombre = nombre;
        this.precio = precio;
        this.stock = stock;
    }

    public String getNombre() {
        return nombre;
    }

    public double getPrecio() {
        return precio;
    }

    public int getStock() {
        return stock;
    }

    @Override
    public String toString() {
        return nombre + " - " + precio + " € - stock: " + stock;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Producto> productos = new ArrayList<>();
        productos.add(new Producto("Ratón", 15.99, 20));
        productos.add(new Producto("Teclado", 29.99, 10));
        productos.add(new Producto("Ratón", 9.99, 50));
        productos.add(new Producto("Monitor", 149.99, 5));

        productos.sort(
                Comparator.comparing(Producto::getNombre)
                        .thenComparingDouble(Producto::getPrecio)
        );

        productos.forEach(producto -> System.out.println(producto));
    }
}

Salida:

Monitor - 149.99  - stock: 5
Ratón - 9.99  - stock: 50
Ratón - 15.99  - stock: 20
Teclado - 29.99  - stock: 10

🧰 Referencias de métodos con métodos propios

No hace falta usar siempre métodos de Java.

También podemos crear nuestros propios métodos y referenciarlos.


🧪 Ejemplo con Consumer

import java.util.List;

public class Main {

    public static void felicitar(String nombre) {
        System.out.println("¡Buen trabajo, " + nombre + "!");
    }

    public static void main(String[] args) {
        List<String> nombres = List.of("Ana", "Luis", "Marta");

        nombres.forEach(Main::felicitar);
    }
}

Salida:

¡Buen trabajo, Ana!
¡Buen trabajo, Luis!
¡Buen trabajo, Marta!

Esto:

Main::felicitar

equivale a:

nombre -> Main.felicitar(nombre)

🧪 Ejemplo con un método que valida

import java.util.function.Predicate;

public class Main {

    public static boolean esNombreLargo(String nombre) {
        return nombre.length() >= 5;
    }

    public static void main(String[] args) {
        Predicate<String> validador = Main::esNombreLargo;

        System.out.println(validador.test("Ana"));
        System.out.println(validador.test("Marta"));
    }
}

Salida:

false
true

Esto:

Main::esNombreLargo

equivale a:

nombre -> Main.esNombreLargo(nombre)

🧠 Cuándo usar referencia de método y cuándo usar lambda

✅ Usa referencia de método cuando...

La lambda solo llama a un método existente.

texto -> texto.toUpperCase()

Mejor:

String::toUpperCase

producto -> producto.getPrecio()

Mejor:

Producto::getPrecio

texto -> Integer.parseInt(texto)

Mejor:

Integer::parseInt

nombre -> new Persona(nombre)

Mejor:

Persona::new

❌ Mejor usa lambda cuando...

La operación tiene más lógica.

producto -> producto.getPrecio() > 10 && producto.getNombre().startsWith("R")

Aquí una referencia de método no sería clara.


También es mejor usar lambda cuando necesitas modificar el valor antes de pasarlo al método.

nombre -> System.out.println("Hola, " + nombre)

Esto no equivale a:

System.out::println

Porque la primera imprime un texto personalizado y la segunda imprime el nombre tal cual.


También es mejor usar lambda cuando necesitas negar una condición.

texto -> !texto.isEmpty()

Existe:

String::isEmpty

pero eso significa:

texto -> texto.isEmpty()

No significa:

texto -> !texto.isEmpty()

🔍 Tabla resumen

Tipo Sintaxis Ejemplo Equivale a
Método estático Clase::metodo Integer::parseInt texto -> Integer.parseInt(texto)
Método de un objeto concreto objeto::metodo registro::guardar texto -> registro.guardar(texto)
Método de instancia Clase::metodo String::toUpperCase texto -> texto.toUpperCase()
Constructor Clase::new Persona::new nombre -> new Persona(nombre)

⚠️ Errores típicos

❌ Error 1: pensar que el método se ejecuta inmediatamente

Supplier<Caja> creador = Caja::new;

Esto no crea una caja en ese momento.

La caja se crea cuando se llama a:

creador.get();

Lo mismo pasa aquí:

nombres.forEach(System.out::println);

println se ejecuta cuando forEach recorre la lista, no al escribir la referencia.


❌ Error 2: usar referencia de método cuando la lambda hace más cosas

Esto se entiende bien como lambda:

nombres.forEach(nombre -> System.out.println("Hola, " + nombre));

No se puede sustituir directamente por:

nombres.forEach(System.out::println);

porque no hace lo mismo.

La primera imprime:

Hola, Ana

La segunda imprime:

Ana

❌ Error 3: confundir método estático con método de instancia

Esto es correcto:

Function<String, String> convertir = String::toUpperCase;

Aunque toUpperCase no sea estático.

Java lo interpreta como:

texto -> texto.toUpperCase()

No significa que se esté llamando a:

String.toUpperCase(); // ❌ Incorrecto

❌ Error 4: usar una referencia que no encaja con lo que Java espera

List<String> nombres = List.of("Ana", "Luis");

nombres.forEach(String::length); // ❌ Error

¿Por qué?

Porque forEach espera algo que reciba un elemento y no devuelva nada.

Pero String::length devuelve un número.

Sí encaja con Function<String, Integer>:

Function<String, Integer> longitud = String::length;

🧪 Ejemplo comparativo final

Versión con lambdas

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

class Alumno {
    private String nombre;
    private double nota;

    public Alumno(String nombre, double nota) {
        this.nombre = nombre;
        this.nota = nota;
    }

    public String getNombre() {
        return nombre;
    }

    public double getNota() {
        return nota;
    }

    @Override
    public String toString() {
        return nombre + " (" + nota + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        List<Alumno> alumnos = new ArrayList<>();
        alumnos.add(new Alumno("Marta", 8.5));
        alumnos.add(new Alumno("Ana", 9.1));
        alumnos.add(new Alumno("Luis", 6.7));

        alumnos.sort((a1, a2) -> a1.getNombre().compareTo(a2.getNombre()));

        alumnos.forEach(alumno -> System.out.println(alumno));
    }
}

Versión con referencias de métodos

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

class Alumno {
    private String nombre;
    private double nota;

    public Alumno(String nombre, double nota) {
        this.nombre = nombre;
        this.nota = nota;
    }

    public String getNombre() {
        return nombre;
    }

    public double getNota() {
        return nota;
    }

    @Override
    public String toString() {
        return nombre + " (" + nota + ")";
    }
}

public class Main {
    public static void main(String[] args) {
        List<Alumno> alumnos = new ArrayList<>();
        alumnos.add(new Alumno("Marta", 8.5));
        alumnos.add(new Alumno("Ana", 9.1));
        alumnos.add(new Alumno("Luis", 6.7));

        alumnos.sort(Comparator.comparing(Alumno::getNombre));

        alumnos.forEach(System.out::println);
    }
}

Salida:

Ana (9.1)
Luis (6.7)
Marta (8.5)

🧭 Cómo pensar cuando veo una referencia de método

Cuando veas algo como esto:

Producto::getPrecio

pregúntate:

  1. ¿Qué dato va a llegar como parámetro?
  2. ¿Qué método se va a llamar?
  3. ¿Qué devuelve ese método?
  4. ¿Encaja con lo que Java espera en ese lugar?

Por ejemplo:

Comparator.comparing(Producto::getPrecio)

Se puede leer así:

Para comparar productos, usa el precio de cada producto.


📝 Mini chuleta

// Método estático
Function<String, Integer> conversor = Integer::parseInt;

// Método estático propio
Consumer<String> accion = Main::felicitar;

// Método de un objeto concreto
Registro registro = new Registro();
Consumer<String> guardar = registro::guardar;

// Método de instancia del objeto recibido como parámetro
Function<String, String> mayusculas = String::toUpperCase;
Function<String, Integer> longitud = String::length;
Predicate<String> vacio = String::isEmpty;

// Constructor sin parámetros
Supplier<Caja> crearCaja = Caja::new;

// Constructor con un parámetro
Function<String, Persona> crearPersona = Persona::new;

// Ordenar por atributo
productos.sort(Comparator.comparing(Producto::getNombre));

// Ordenar por atributo numérico
productos.sort(Comparator.comparingDouble(Producto::getPrecio));

✅ Conclusión

Las referencias de métodos hacen que el código sea más limpio cuando la lambda simplemente llama a un método existente.

La clave es esta:

texto -> texto.toUpperCase()

se puede escribir como:

String::toUpperCase

Y esto:

nombre -> new Persona(nombre)

se puede escribir como:

Persona::new

Pero no conviene forzarlo siempre.

Regla sencilla:

Si la lambda solo llama a un método, probablemente puedas usar una referencia de método.
Si la lambda tiene lógica propia, condiciones, concatenaciones o varias instrucciones, mejor deja la lambda.