Referências de Método em Java
Use referências de método em Java com o operador :: — estáticas, de instância, vinculadas e de construtor.
Uma referência de método é uma sintaxe abreviada para uma lambda cujo corpo não faz nada além de chamar um método existente. Quando uma lambda é literalmente x -> SomeClass.foo(x) ou (a, b) -> a.bar(b), o operador :: permite escrever como SomeClass::foo ou Some::bar. O compilador produz o mesmo valor das duas formas — uma instância da interface funcional adequada — portanto, onde uma lambda se encaixa, uma referência de método compatível também se encaixa.
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 referenceAs quatro formas abaixo cobrem todas as referências de método que você escreverá. A única habilidade necessária é reconhecer qual forma se aplica a um determinado ponto de chamada.
Forma 1: Referência a método estático — ClassName::staticMethod
O método é um método static em uma classe. A referência se torna uma lambda cujos parâmetros são os parâmetros do 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 é a forma que aparece em código de stream como nums.stream().reduce(0, Integer::sum) — Integer.sum(int, int) é estático, então Integer::sum é um BinaryOperator<Integer> (um BiFunction<Integer, Integer, Integer>).
Forma 2: Referência a método de instância vinculada — instance::method
O método é um método de instância em um objeto específico e nomeado. A referência se torna uma lambda cujos parâmetros são os parâmetros do método (a instância é 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)O receptor vinculado preenche o slot sem argumento: como prefix já está capturado, greet só precisa do argumento para concat, tornando-se uma Function<String, String> em vez de uma BiFunction. Da mesma forma, log::add mantém log fixo e expõe apenas o elemento a ser adicionado, resultando em um Consumer<String>.
A instance capturada é mantida pelo objeto resultante, de forma semelhante a como uma lambda captura variáveis locais effectively final. Referências vinculadas são a maneira de dizer "use o método deste objeto como callback" — Logger::info em um logger específico, event::handle em um manipulador específico.
Forma 3: Referência a método de instância não vinculada — ClassName::method
O método é um método de instância, mas você o referencia por meio da classe em vez de uma instância específica. A referência se torna uma lambda cujo primeiro parâmetro é o receptor, e o restante são os próprios parâmetros do 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 é a forma que confunde as pessoas. String::length parece que poderia significar "o método length na classe String" — mas não existe tal método estático. Na verdade, significa "dado qualquer String, chame seu método de instância length() — o receptor é o primeiro parâmetro da lambda." É por isso que String::length é uma Function<String, Integer> (uma entrada, uma saída) e String::startsWith é uma BiPredicate<String, String> (a segunda entrada é o prefixo que o receptor testa).
Esta forma é o motor por trás de quase todo pipeline de stream:
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: Referência a construtor — ClassName::new
Referencia um construtor como uma função. A lambda resultante recebe os parâmetros do construtor e retorna uma nova instância:
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;Referências a construtores são a forma como Collectors.toCollection(TreeSet::new) permite escolher um tipo de destino, e como Stream.generate(Random::new) produz objetos Random independentes a cada chamada de get().
Arrays têm uma forma especial: String[]::new é uma IntFunction<String[]> — n -> new String[n]. É isso que stream.toArray(String[]::new) utiliza.
Referência de método vs lambda — quando cada uma é melhor
Uma referência de método é a escolha certa quando o corpo da lambda é exatamente uma única chamada de método com os parâmetros passados na ordem:
| Lambda | Referência de 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 |
Uma lambda é a escolha certa quando o corpo faz qualquer outra coisa:
- Chama mais de um método:
s -> s.trim().toUpperCase()(sem referência para a cadeia). - Tem qualquer transformação de argumento:
s -> System.out.println("[" + s + "]"). - Tem qualquer fluxo de controle:
n -> n < 0 ? 0 : n. - Reordena ou duplica argumentos:
(a, b) -> b.compareTo(a)(comparador invertido).
A otimização não é sobre velocidade em tempo de execução — ambas compilam para o mesmo bootstrap invokedynamic. É sobre legibilidade. Person::name salta aos olhos como "o campo name," enquanto p -> p.name() exige a leitura de três tokens. Quando a referência se encaixa, prefira-a; quando não, não torça o código para forçá-la.
Um problema com referências a construtor: sobrecargas ambíguas
ClassName::new funciona bem quando há um construtor correspondendo à interface alvo. Quando há vários, o compilador escolhe com base na contagem e nos tipos de parâmetros do tipo alvo. Na maioria das vezes isso funciona; ocasionalmente não funciona, e você precisa desambiguar tipando a variável explicitamente ou voltando a uma 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;A solução é manter o tipo alvo explícito, como em a, b, c acima.
Um exemplo completo: as quatro formas em um único programa
O programa abaixo constrói e usa uma referência de método de cada forma, demonstra como String::length (não vinculada) se torna uma Function<String, Integer> e mostra o truque da referência a construtor que impulsiona stream().toArray(T[]::new).
O que observar na execução:
- Todas as quatro formas compilam para instâncias de interfaces funcionais comuns —
parseé umaFunction<String, Integer>tanto se você escreveus -> Integer.parseInt(s)quantoInteger::parseInt. A abreviação é puramente sintática. - As formas não vinculadas
String::lengtheString::toUpperCaseambas têm um receptor como primeiro parâmetro. É por isso queString::lengthé umaFunction<String, Integer>eString::startsWithé umaBiPredicate<String, String>— o receptor é um slot, e o parâmetro explícito é o outro. - A referência a construtor
String[]::newproduziu umaIntFunction<String[]>— a forma questream().toArray(...)espera. Referências a construtor são a maneira de dizer a um stream "aqui está o tipo de destino." - O comparador de tamanho invertido não poderia ser escrito como uma referência de método: o receptor e o parâmetro se invertem, e referências de método não podem reordenar argumentos. Esse é exatamente o tipo de caso em que uma lambda ainda é a escolha certa.
O que vem a seguir
Agora você pode escrever um pipeline de stream quase inteiramente em referências de método e deixar as poucas transformações que genuinamente precisam de formatação viverem em pequenas lambdas. Esse estilo é a introdução natural à peça central desta parte: os streams. O próximo capítulo, Introdução ao Java Streams, apresenta a API Stream<T> — o que é, como é um pipeline de stream, por que é lazy, por que só pode ser usado uma vez e como se encaixa com as lambdas, interfaces funcionais e referências de método que você acabou de aprender.