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.

Você vai aprender
  • 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 reference e ';' 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 n e só o repassa para println.
System.out::println
"Use o método println do objeto System.out." O parâmetro não aparece: o Java entende que cada elemento deve ser passado para println.
::
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ênciaLambda equivalente
Integer::parseInts -> Integer.parseInt(s)
Math::absx -> Math.abs(x)
Main::aprovadon -> 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ênciaLambda equivalente
System.out::printlnx -> System.out.println(x)
prefixo::concats -> prefixo.concat(s)
chamada::addx -> chamada.add(x)
this::calcularx -> 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 s que chegar, chame s.toUpperCase()." O s é 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 como Comparator.
ReferênciaLambda equivalente
String::toUpperCases -> s.toUpperCase()
String::lengths -> s.length()
String::isEmptys -> s.isEmpty()
String::compareToIgnoreCase(a, b) -> a.compareToIgnoreCase(b)
Aluno::nota (visto em Comparator)a -> a.nota()
Tipo 1 ou tipo 3?

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ênciaUsada comoLambda equivalente
ArrayList::newSupplier<List<String>>() -> new ArrayList<>()
Aluno::newFunction<String, Aluno>nome -> new Aluno(nome)
StringBuilder::newFunction<String, StringBuilder>s -> new StringBuilder(s)
int[]::newIntFunction<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

TipoFormaExemploLambda equivalente
1. Método estáticoClasse::metodoEstaticoInteger::parseInts -> Integer.parseInt(s)
2. Método de um objetoobjeto::metodoSystem.out::printlnx -> System.out.println(x)
3. Método de instância de qualquer objetoClasse::metodoDeInstanciaString::lengths -> s.length()
3. (com dois parâmetros)Classe::metodoDeInstanciaString::compareTo(a, b) -> a.compareTo(b)
4. ConstrutorClasse::newAluno::newn -> new Aluno(n)

Referência ou lambda?

SituaçãoPrefira
A lambda só repassa os parâmetros, na mesma ordem, para um métodoReferê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 compridaCrie um método com nome e use Classe::metodo
A referência ficou mais difícil de entender do que a lambdaLambda. Clareza vem primeiro.

Quando dá errado

"';' expected": parênteses depois do nome

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.

"invalid method reference … reference to toString is ambiguous"

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.

"invalid method reference … unexpected instance method … found in unbound lookup"

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:

nomes.forEach();

2. Converter

Complete com a referência equivalente a s -> Integer.parseInt(s):

Function<String, Integer> f = ;

3. Fábrica

Complete com a referência ao construtor de Aluno:

Function<String, Aluno> criar = ;

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) e Classe::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.