Java Referências de Métodos
Muitas lambdas não fazem nada além de chamar um método que já existe: n -> System.out.println(n), s -> s.toUpperCase(), nome -> new Aluno(nome). Para esses casos, o Java tem um atalho: a referência de método, escrita com dois-pontos duplos (::). Em vez de descrever o que fazer, você aponta para o método que faz: System.out::println.
- O que é o operador
::e quando ele pode substituir uma lambda. - Os quatro tipos: método estático, método de um objeto, método de instância "de qualquer um" e construtor.
- A tabela de equivalência: cada referência e a lambda que ela substitui.
- Quando é melhor continuar com a lambda.
- Os erros
invalid method referencee';' expected.
A ideia: apontar em vez de explicar
Se alguém pergunta como chegar à biblioteca, você pode explicar o caminho todo ("vire à direita, ande duas quadras…") ou simplesmente apontar para o mapa da escola onde o caminho já está desenhado. A lambda explica; a referência de método aponta.
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> nomes = List.of("Ana", "Bruno", "Carla");
nomes.forEach(n -> System.out.println(n));
nomes.forEach(System.out::println);
}
}
$ javac Main.java $ java Main Ana Bruno Carla Ana « a segunda forma faz exatamente o mesmo Bruno Carla
- n -> System.out.println(n)
- A lambda recebe
ne só o repassa paraprintln. - System.out::println
- "Use o método
printlndo objetoSystem.out." O parâmetro não aparece: o Java entende que cada elemento deve ser passado paraprintln. - ::
- O operador de referência de método. Atenção: sem parênteses depois do nome do método. Você não está chamando o método agora; está indicando qual método chamar depois.
Assim como a lambda, uma referência de método só pode ser usada onde se espera uma interface funcional. O compilador confere se o método apontado "encaixa" no método da interface (mesmos parâmetros, retorno compatível).
Tipo 1: método estático — Classe::metodoEstatico
Aponta para um método estático. Os parâmetros da lambda viram os argumentos do método.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
public class Main {
static boolean aprovado(int nota) {
return nota >= 7;
}
public static void main(String[] args) {
Function<String, Integer> converter = Integer::parseInt;
int nota = converter.apply("8") + 1;
System.out.println("nota + 1 = " + nota);
Function<Integer, Integer> absoluto = Math::abs;
System.out.println("abs(-5) = " + absoluto.apply(-5));
Predicate<Integer> passou = Main::aprovado;
List<Integer> notas = new ArrayList<>(List.of(9, 4, 7, 5));
notas.removeIf(passou.negate());
System.out.println("aprovadas: " + notas);
}
}
$ javac Main.java $ java Main nota + 1 = 9 abs(-5) = 5 aprovadas: [9, 7]
| Referência | Lambda equivalente |
|---|---|
Integer::parseInt | s -> Integer.parseInt(s) |
Math::abs | x -> Math.abs(x) |
Main::aprovado | n -> Main.aprovado(n) |
Integer::sum (visto no HashMap) | (a, b) -> Integer.sum(a, b) |
Repare em Main::aprovado: você pode apontar para os seus próprios métodos. É uma ótima forma de dar nome a uma regra: removeIf(Main::reprovado) se lê melhor que uma lambda comprida.
Tipo 2: método de um objeto específico — objeto::metodo
Aponta para um método de um objeto que já existe. Os parâmetros da lambda viram os argumentos; o objeto é sempre o mesmo. System.out::println é desse tipo: System.out é um objeto (o "terminal").
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
String prefixo = "Prof. ";
Function<String, String> titulo = prefixo::concat;
System.out.println(titulo.apply("Ana"));
List<String> chamada = new ArrayList<>();
Consumer<String> matricular = chamada::add;
matricular.accept("Bruno");
matricular.accept("Carla");
System.out.println(chamada);
List.of("x", "y").forEach(System.out::println);
}
}
$ javac Main.java
$ java Main
Prof. Ana « prefixo.concat("Ana")
[Bruno, Carla] « chamada.add("Bruno"), chamada.add("Carla")
x
y
| Referência | Lambda equivalente |
|---|---|
System.out::println | x -> System.out.println(x) |
prefixo::concat | s -> prefixo.concat(s) |
chamada::add | x -> chamada.add(x) |
this::calcular | x -> this.calcular(x) (dentro de um método de objeto) |
Tipo 3: método de instância de "qualquer um" — Classe::metodoDeInstancia
Este é o que mais confunde. Parece o tipo 1 (Classe:: na frente), mas o método não é estático. Então, em qual objeto ele é chamado? No primeiro parâmetro da lambda: cada elemento que chega vira o objeto que executa o método.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<String, String> maiusculas = String::toUpperCase;
Function<String, Integer> tamanho = String::length;
System.out.println(maiusculas.apply("ana") + " " + tamanho.apply("ana"));
List<String> nomes = new ArrayList<>(List.of("carla", "Bruno", "ana"));
nomes.sort(String::compareToIgnoreCase);
System.out.println(nomes);
nomes.sort(Comparator.comparing(String::length).reversed());
System.out.println(nomes);
nomes.replaceAll(String::toUpperCase);
System.out.println(nomes);
}
}
$ javac Main.java $ java Main ANA 3 [ana, Bruno, carla] « a.compareToIgnoreCase(b) [Bruno, carla, ana] « do maior nome para o menor [BRUNO, CARLA, ANA]
- String::toUpperCase
- "Para cada String
sque chegar, chames.toUpperCase()." Osé o próprio objeto, não um argumento. - String::length
s -> s.length().- String::compareToIgnoreCase
- Com dois parâmetros: o primeiro é o objeto, o segundo vira argumento:
(a, b) -> a.compareToIgnoreCase(b). Por isso serve comoComparator.
| Referência | Lambda equivalente |
|---|---|
String::toUpperCase | s -> s.toUpperCase() |
String::length | s -> s.length() |
String::isEmpty | s -> s.isEmpty() |
String::compareToIgnoreCase | (a, b) -> a.compareToIgnoreCase(b) |
Aluno::nota (visto em Comparator) | a -> a.nota() |
Os dois começam com o nome da classe. A diferença está no método: se ele é static (Integer.parseInt, Math.abs), é o tipo 1 e todos os parâmetros viram argumentos. Se é de instância ("ana".toUpperCase()), é o tipo 3 e o primeiro parâmetro vira o objeto. Você não precisa decorar: o compilador descobre sozinho.
Missão: troque as duas lambdas por referências de métodos (String::toUpperCase e System.out::println). A saída deve continuar ANA, BRUNO, CARLA.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> nomes = new ArrayList<>(List.of("ana", "bruno", "carla"));
nomes.replaceAll(n -> n.toUpperCase());
nomes.forEach(n -> System.out.println(n));
}
}
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> nomes = new ArrayList<>(List.of("ana", "bruno", "carla"));
nomes.replaceAll(String::toUpperCase);
nomes.forEach(System.out::println);
}
}
Tipo 4: construtor — Classe::new
Aponta para um construtor. Os parâmetros da lambda viram os argumentos do new.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
record Aluno(String nome) { }
public class Main {
public static void main(String[] args) {
Supplier<List<String>> novaLista = ArrayList::new;
List<String> l = novaLista.get();
l.add("primeiro");
System.out.println(l);
Function<String, Aluno> criar = Aluno::new;
Aluno a = criar.apply("Duda");
System.out.println(a);
Function<String, StringBuilder> sb = StringBuilder::new;
System.out.println(sb.apply("abc").reverse());
List<Aluno> turma = new ArrayList<>();
for (String nome : List.of("Ana", "Bruno")) {
turma.add(criar.apply(nome));
}
System.out.println(turma);
}
}
$ javac Main.java $ java Main [primeiro] Aluno[nome=Duda] cba [Aluno[nome=Ana], Aluno[nome=Bruno]]
| Referência | Usada como | Lambda equivalente |
|---|---|---|
ArrayList::new | Supplier<List<String>> | () -> new ArrayList<>() |
Aluno::new | Function<String, Aluno> | nome -> new Aluno(nome) |
StringBuilder::new | Function<String, StringBuilder> | s -> new StringBuilder(s) |
int[]::new | IntFunction<int[]> | n -> new int[n] |
Repare que StringBuilder::new poderia apontar para vários construtores (com texto, sem nada, com capacidade). O compilador escolhe o que encaixa no tipo: Function<String, …> recebe uma String, então é new StringBuilder(String). Referências a construtores aparecem muito com Collectors (Collectors.toCollection(TreeSet::new)).
A tabela completa
| Tipo | Forma | Exemplo | Lambda equivalente |
|---|---|---|---|
| 1. Método estático | Classe::metodoEstatico | Integer::parseInt | s -> Integer.parseInt(s) |
| 2. Método de um objeto | objeto::metodo | System.out::println | x -> System.out.println(x) |
| 3. Método de instância de qualquer objeto | Classe::metodoDeInstancia | String::length | s -> s.length() |
| 3. (com dois parâmetros) | Classe::metodoDeInstancia | String::compareTo | (a, b) -> a.compareTo(b) |
| 4. Construtor | Classe::new | Aluno::new | n -> new Aluno(n) |
Referência ou lambda?
| Situação | Prefira |
|---|---|
| A lambda só repassa os parâmetros, na mesma ordem, para um método | Referência: String::length |
A lambda faz uma conta ou combina coisas (n -> n * 2, s -> "[" + s + "]") | Lambda (não existe método pronto para apontar) |
A lambda passa um valor fixo (s -> s.startsWith("B")) | Lambda |
| A lambda ficou comprida | Crie um método com nome e use Classe::metodo |
| A referência ficou mais difícil de entender do que a lambda | Lambda. Clareza vem primeiro. |
Quando dá errado
Este código tem um erro de propósito.
Function<String, Integer> tamanho = String::length();
$ javac Main.java
Main.java:5: error: ';' expected
Function<String, Integer> tamanho = String::length();
^
1 error
O que quer dizer: depois de String::length o compilador esperava o fim da instrução. Numa referência de método você não chama o método, então não há parênteses.
Como consertar: String::length.
Este código tem um erro de propósito.
Function<Integer, String> texto = Integer::toString;
$ javac Main.java
Main.java:5: error: incompatible types: invalid method reference
Function<Integer, String> texto = Integer::toString;
^
reference to toString is ambiguous
both method toString(int) in Integer and method toString() in Integer match
1 error
O que quer dizer: "a referência a toString é ambígua". Integer tem um toString(int) estático (tipo 1: n -> Integer.toString(n)) e um toString() de instância (tipo 3: n -> n.toString()). Os dois encaixam, e o compilador não escolhe por você.
Como consertar: use uma lambda, que é explícita: n -> n.toString(), ou outro método: String::valueOf.
Este código tem um erro de propósito.
Supplier<Integer> tamanho = String::length;
$ javac Main.java
Main.java:5: error: incompatible types: invalid method reference
Supplier<Integer> tamanho = String::length;
^
unexpected instance method length() found in unbound lookup
1 error
O que quer dizer: String::length é do tipo 3: precisa de uma String para ser chamado. Mas um Supplier não recebe nada, então não há de qual String medir o tamanho.
Como consertar: use uma interface que receba a String (Function<String, Integer>), ou aponte para um objeto específico (tipo 2): "Carla"::length.
Exercícios
1. Mostrar cada um
Complete com uma referência de método para mostrar cada elemento:
2. Converter
Complete com a referência equivalente a s -> Integer.parseInt(s):
3. Fábrica
Complete com a referência ao construtor de Aluno:
4. String::trim equivale a qual lambda?
5. Qual lambda não pode virar uma referência de método simples?
Missão final: crie a fábrica de produtos com uma referência ao construtor (Produto::new) no lugar da lambda. A saída deve continuar [Produto[nome=caderno], Produto[nome=lápis]].
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
record Produto(String nome) { }
public class Main {
public static void main(String[] args) {
Function<String, Produto> fabrica = nome -> new Produto(nome);
List<Produto> estoque = new ArrayList<>();
for (String nome : List.of("caderno", "lápis")) {
estoque.add(fabrica.apply(nome));
}
System.out.println(estoque);
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
record Produto(String nome) { }
public class Main {
public static void main(String[] args) {
Function<String, Produto> fabrica = Produto::new;
List<Produto> estoque = new ArrayList<>();
for (String nome : List.of("caderno", "lápis")) {
estoque.add(fabrica.apply(nome));
}
System.out.println(estoque);
}
}
Resumo
- Uma referência de método (
::) substitui uma lambda que só chama um método existente. - Quatro tipos:
Classe::estatico,objeto::metodo,Classe::metodoDeInstancia(o 1º parâmetro vira o objeto) eClasse::new. - Sem parênteses depois do nome: você aponta, não chama.
- Só funciona onde se espera uma interface funcional, e o método precisa encaixar nela.
- Se a referência ficar ambígua ou menos clara, volte para a lambda.
Próxima aula: Streams, onde lambdas e referências de métodos viram uma linha de montagem de dados.