Referencias a métodos en Java
Usa el operador :: en Java para referenciar métodos como lambdas: estáticos, de instancia, ligados y referencias a constructores.
Una referencia a método es una sintaxis abreviada para una lambda cuyo cuerpo no hace más que llamar a un método existente. Cuando una lambda es literalmente x -> SomeClass.foo(x) o (a, b) -> a.bar(b), el operador :: permite escribirla como SomeClass::foo o Some::bar. El compilador produce el mismo valor de cualquier manera — una instancia de la interfaz funcional adecuada — así que en cualquier lugar donde encaje una lambda, también encaja una referencia a método equivalente.
Function<String, Integer> len1 = s -> s.length();
Function<String, Integer> len2 = String::length; // identical at runtime
List<String> names = List.of("Bob", "Alice");
names.forEach(s -> System.out.println(s)); // lambda
names.forEach(System.out::println); // method referenceLas cuatro formas siguientes cubren todas las referencias a métodos que escribirás. La única habilidad necesaria es reconocer qué forma encaja en un punto de uso determinado.
Forma 1: Referencia a método estático — ClassName::staticMethod
El método es un método static de una clase. La referencia se convierte en una lambda cuyos parámetros son los parámetros del método estático:
Function<String, Integer> parse = Integer::parseInt; // s -> Integer.parseInt(s)
BinaryOperator<Integer> max = Math::max; // (a, b) -> Math.max(a, b)
Function<Object, String> toStr = String::valueOf; // o -> String.valueOf(o)Esta es la forma que aparece en código de streams como nums.stream().reduce(0, Integer::sum) — Integer.sum(int, int) es estático, por lo que Integer::sum es un BinaryOperator<Integer> (un BiFunction<Integer, Integer, Integer>).
Forma 2: Referencia a método de instancia ligado — instance::method
El método es un método de instancia de un objeto específico y con nombre. La referencia se convierte en una lambda cuyos parámetros son los parámetros del método (la instancia queda capturada):
PrintStream out = System.out;
Consumer<String> print = out::println; // s -> out.println(s)
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat; // name -> prefix.concat(name)
List<String> log = new ArrayList<>();
Consumer<String> record = log::add; // msg -> log.add(msg)El receptor ligado ocupa el hueco sin argumento: como prefix ya está capturado, greet solo necesita el argumento de concat, por lo que es una Function<String, String> en lugar de una BiFunction. De forma similar, log::add mantiene fijo log y solo expone el elemento a añadir, dando un Consumer<String>.
La instance capturada es retenida por el objeto resultante, de manera similar a cómo una lambda captura variables locales effectively final. Las referencias ligadas son la manera de decir "usa el método de este objeto como callback" — Logger::info sobre un logger concreto, event::handle sobre un handler concreto.
Forma 3: Referencia a método de instancia no ligado — ClassName::method
El método es un método de instancia, pero se referencia a través de la clase en lugar de una instancia concreta. La referencia se convierte en una lambda cuyo primer parámetro es el receptor, y el resto son los parámetros propios del método:
Function<String, Integer> len = String::length; // s -> s.length() — first param is the receiver
Function<String, String> upper = String::toUpperCase; // s -> s.toUpperCase()
BiPredicate<String, String> starts = String::startsWith; // (s, prefix) -> s.startsWith(prefix)Esta es la forma que confunde a la gente. String::length parece que podría significar "el método length de la clase String" — pero no existe tal método estático. En realidad significa "dado cualquier String, llama a su método de instancia length() — el receptor es el primer parámetro de la lambda". Por eso String::length es una Function<String, Integer> (una entrada, una salida) y String::startsWith es un BiPredicate<String, String> (la segunda entrada es el prefijo que prueba el receptor).
Esta forma es el motor que impulsa prácticamente cualquier pipeline de streams:
people.stream()
.map(Person::name) // unbound: p -> p.name()
.filter(s -> s.startsWith("A"))
.map(String::toUpperCase) // unbound: s -> s.toUpperCase()
.forEach(System.out::println); // bound: s -> out.println(s)Forma 4: Referencia a constructor — ClassName::new
Referencia un constructor como una función. La lambda resultante toma los parámetros del constructor y devuelve una nueva instancia:
Supplier<List<String>> listOf = ArrayList::new; // () -> new ArrayList<>()
Function<Integer, ArrayList<?>> sized = ArrayList::new; // n -> new ArrayList<>(n)
Function<String, BigDecimal> toBig = BigDecimal::new; // s -> new BigDecimal(s)
BiFunction<String, Integer, AbstractMap.SimpleEntry<String, Integer>> entry =
AbstractMap.SimpleEntry::new;Las referencias a constructor son la forma en que Collectors.toCollection(TreeSet::new) permite elegir un tipo de destino, y cómo Stream.generate(Random::new) produce objetos Random independientes en cada llamada a get().
Los arrays tienen una forma especial: String[]::new es una IntFunction<String[]> — n -> new String[n]. Esto es lo que usa stream.toArray(String[]::new).
Referencia a método vs lambda — cuándo usar cada una
Una referencia a método es la opción correcta cuando el cuerpo de la lambda es exactamente una única llamada a método con los parámetros pasados en orden:
| Lambda | Referencia a método |
|---|---|
s -> s.length() | String::length |
s -> System.out.println(s) | System.out::println |
(a, b) -> a.compareTo(b) | String::compareTo |
() -> new ArrayList<>() | ArrayList::new |
Una lambda es la opción correcta cuando el cuerpo hace cualquier otra cosa:
- Llama a más de un método:
s -> s.trim().toUpperCase()(no hay referencia para la cadena). - Tiene alguna transformación de argumento:
s -> System.out.println("[" + s + "]"). - Tiene algún flujo de control:
n -> n < 0 ? 0 : n. - Reordena o duplica argumentos:
(a, b) -> b.compareTo(a)(comparador invertido).
La optimización no tiene que ver realmente con la velocidad en tiempo de ejecución — ambas se compilan al mismo bootstrap invokedynamic. Se trata de legibilidad. Person::name salta a la vista como "el campo name", mientras que p -> p.name() obliga a leer tres tokens. Cuando la referencia encaja, úsala; cuando no, no fuerces el código para que encaje.
Una trampa con las referencias a constructor: sobrecargas ambiguas
ClassName::new funciona bien cuando hay un único constructor que coincide con la interfaz destino. Cuando hay varios, el compilador elige basándose en el recuento de parámetros y los tipos del tipo destino. La mayoría de las veces funciona; ocasionalmente no, y hay que desambiguar tipando la variable explícitamente o recurriendo a una lambda:
// ArrayList has constructors: (), (int), (Collection)
Supplier<ArrayList<String>> a = ArrayList::new; // picks the no-arg
Function<Integer, ArrayList<String>> b = ArrayList::new; // picks the (int) one
Function<List<String>, ArrayList<String>> c = ArrayList::new; // picks the (Collection) one
// var inference can't disambiguate — this would not compile:
// var ambiguous = ArrayList::new;La solución es mantener el tipo destino explícito, como en a, b, c arriba.
Un ejemplo práctico: las cuatro formas en un solo programa
El programa siguiente construye y utiliza una referencia a método de cada forma, demuestra cómo String::length (no ligada) se convierte en una Function<String, Integer>, y muestra el truco de la referencia a constructor que impulsa stream().toArray(T[]::new).
Lo que se extrae de la ejecución:
- Las cuatro formas se compilan en instancias de interfaces funcionales ordinarias —
parsees unaFunction<String, Integer>tanto si escribistes -> Integer.parseInt(s)comoInteger::parseInt. La abreviatura es puramente sintáctica. String::lengthyString::toUpperCaseno ligadas tienen ambas un receptor como primer parámetro. Por esoString::lengthes unaFunction<String, Integer>yString::startsWithes unBiPredicate<String, String>— el receptor ocupa un hueco, y el parámetro explícito, el otro.- La referencia a constructor
String[]::newprodujo unaIntFunction<String[]>— la forma que quierestream().toArray(...). Las referencias a constructor son la manera de decirle a un stream "aquí está el tipo destino". - El comparador de longitud invertido no podía escribirse como referencia a método: el receptor y el parámetro se intercambian, y las referencias a método no pueden reordenar argumentos. Es exactamente el tipo de caso en que una lambda sigue siendo la opción correcta.
Qué viene después
Ahora puedes escribir un pipeline de streams casi completamente con referencias a métodos y dejar que las pocas transformaciones que realmente necesitan forma vivan en pequeñas lambdas. Ese estilo es la antesala natural de la pieza central de esta parte: los streams. El siguiente capítulo, Introducción a Java Streams, presenta la API Stream<T> — qué es, cómo es un pipeline de streams, por qué es lazy, por qué solo puede usarse una vez y cómo encaja junto con las lambdas, interfaces funcionales y referencias a métodos que acabas de aprender.