Java Interfaces Funcionais

Na aula de Lambdas você criou interfaces como Operacao só para ter onde "encaixar" uma lambda. Na prática, quase nunca é preciso: o Java traz, no pacote java.util.function, um conjunto de interfaces funcionais prontas para os formatos mais comuns de função: "recebe um e devolve outro", "responde sim ou não", "só faz alguma coisa", "fabrica um valor"… Nesta aula você vai conhecer as sete principais, aprender a combiná-las e a criar a sua quando nenhuma servir.

Você vai aprender
  • O que é uma interface funcional e o que faz a anotação @FunctionalInterface.
  • Function e BiFunction (transformar), Predicate (testar), Consumer (usar), Supplier (fornecer).
  • UnaryOperator e BinaryOperator (quando entrada e saída são do mesmo tipo).
  • Compor funções: andThen, compose, and, or, negate.
  • Escrever métodos que recebem funções, e quando criar a sua própria interface.

A ideia: tomadas padronizadas

Uma lambda é como um aparelho elétrico, e uma interface funcional é a tomada onde ele encaixa. Se cada fabricante inventasse a sua tomada, nada se conectaria com nada. Por isso o Java padronizou as tomadas mais comuns. Quando um método da biblioteca pede um Predicate<String>, você já sabe que ele quer "uma função que recebe um texto e responde true ou false", e qualquer lambda nesse formato serve.

Relembrando a definição: interface funcional é uma interface com exatamente um método abstrato. Ela pode ter quantos métodos default e static quiser (é assim que Predicate ganha and e negate, como você vai ver).


Criando a sua: @FunctionalInterface

Comecemos pela que você já sabe fazer, agora com uma anotação nova:

@FunctionalInterface
interface Desconto {
    double aplicar(double preco);
}

public class Main {
    public static void main(String[] args) {
        Desconto blackFriday = preco -> preco * 0.5;
        Desconto clienteFiel = preco -> preco - 10;
        System.out.println(blackFriday.aplicar(80.0));
        System.out.println(clienteFiel.aplicar(80.0));
    }
}
$ javac Main.java
$ java Main
40.0
70.0

A anotação @FunctionalInterface não muda o funcionamento: ela é um aviso ao compilador: "esta interface é para lambdas; me avise se alguém estragar isso". Se um colega acrescentar um segundo método abstrato:

Este código tem um erro de propósito.

@FunctionalInterface
interface Desconto {
    double aplicar(double preco);
    String descricao();
}
$ javac Main.java
Main.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  Desconto is not a functional interface
    multiple non-overriding abstract methods found in interface Desconto
1 error

"Anotação @FunctionalInterface inesperada: Desconto não é uma interface funcional; foram encontrados vários métodos abstratos." Sem a anotação, o erro só apareceria depois, em cada lugar onde uma lambda fosse usada como Desconto. Com ela, aparece no lugar certo: na interface.


As interfaces prontas

Todas moram em java.util.function. O nome de cada uma descreve o formato da função, e cada uma tem o seu método (que você chama para "executar" a lambda):

InterfaceRecebeDevolveMétodoIdeiaExemplo
Function<T, R>um Tum Rapplytransformars -> s.length()
BiFunction<T, U, R>T e Uum Rapplycombinar dois(preco, qtd) -> preco * qtd
Predicate<T>um Tbooleantesttestar, filtrarn -> n >= 7
Consumer<T>um Tnadaacceptusar, fazer algos -> System.out.println(s)
Supplier<T>nadaum Tgetfornecer, fabricar() -> new ArrayList<>()
UnaryOperator<T>um Tum Tapplytransformar sem mudar o tipos -> s.toUpperCase()
BinaryOperator<T>dois Tum Tapplyjuntar dois do mesmo tipo(a, b) -> a + b

Nos < >, como você viu em Generics: T é o tipo de entrada, R (de result) o de saída, U o segundo de entrada. Vamos a cada uma.

Function e BiFunction: transformar

import java.util.function.BiFunction;
import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, Integer> tamanho = s -> s.length();
        Function<Double, String> conceito = nota -> nota >= 7 ? "aprovado" : "recuperação";
        BiFunction<Double, Integer, Double> total = (preco, qtd) -> preco * qtd;

        System.out.println(tamanho.apply("Carla"));
        System.out.println(conceito.apply(8.5));
        System.out.println(conceito.apply(4.0));
        System.out.println(total.apply(2.5, 4));
    }
}
$ javac Main.java
$ java Main
5
aprovado
recuperação
10.0
Function<String, Integer>
"Função de String para Integer": recebe um texto, devolve um número.
tamanho.apply("Carla")
apply = aplicar. É assim que se executa a função guardada na variável.
BiFunction<Double, Integer, Double>
"Bi" = dois: recebe um Double (preço) e um Integer (quantidade), devolve um Double. O último tipo é sempre o do resultado.

Predicate: testar (sim ou não)

Um predicado, em lógica, é uma afirmação que pode ser verdadeira ou falsa sobre algo: "a nota é maior ou igual a 7". É a interface usada por removeIf e pelo filter dos streams.

import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Double> aprovado = n -> n >= 7;
        Predicate<Double> nota10 = n -> n == 10;
        Predicate<Double> valida = n -> n >= 0 && n <= 10;

        System.out.println("aprovado.test(8.0): " + aprovado.test(8.0));
        System.out.println("aprovado.test(5.5): " + aprovado.test(5.5));
        System.out.println("negate().test(5.5): " + aprovado.negate().test(5.5));
        System.out.println("valida.and(aprovado).test(11.0): " + valida.and(aprovado).test(11.0));
        System.out.println("nota10.or(n -> n < 2).test(1.0): " + nota10.or(n -> n < 2).test(1.0));

        List<Double> notas = new ArrayList<>(List.of(9.5, 4.0, 7.0, 12.0, 6.5));
        notas.removeIf(valida.negate());
        System.out.println("notas válidas: " + notas);
        notas.removeIf(aprovado);
        System.out.println("em recuperação: " + notas);
    }
}
$ javac Main.java
$ java Main
aprovado.test(8.0): true
aprovado.test(5.5): false
negate().test(5.5): true                    « negate = "o contrário"
valida.and(aprovado).test(11.0): false      « 11 passa em aprovado, mas não em valida
nota10.or(n -> n < 2).test(1.0): true
notas válidas: [9.5, 4.0, 7.0, 6.5]         « o 12.0 saiu
em recuperação: [4.0, 6.5]
MétodoEquivale aDevolve
p.test(x)executar o testeboolean
p.negate()!p ("não")um novo Predicate
p.and(q)p && q ("e")um novo Predicate
p.or(q)p || q ("ou")um novo Predicate
Predicate.not(p) Java 11+o mesmo que p.negate()um novo Predicate

Esses métodos não testam nada: eles montam um predicado novo a partir dos existentes, como peças de Lego. O teste só acontece no test (ou quando o removeIf chama o test por você). Compare com os operadores lógicos &&, ||, !: a ideia é a mesma, só que com regras guardadas em variáveis.

Missão: complete o Predicate<Double> recuperacao: verdadeiro para notas de 5 (inclusive) até 7 (exclusive). A saída deve ser 6.0 em recuperação? true e 8.0 em recuperação? false.

import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Double> recuperacao = n -> false;
        System.out.println("6.0 em recuperação? " + recuperacao.test(6.0));
        System.out.println("8.0 em recuperação? " + recuperacao.test(8.0));
    }
}
import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Double> recuperacao = n -> n >= 5 && n < 7;
        System.out.println("6.0 em recuperação? " + recuperacao.test(6.0));
        System.out.println("8.0 em recuperação? " + recuperacao.test(8.0));
    }
}

Consumer: usar (sem devolver nada)

Um consumidor recebe um valor e faz alguma coisa com ele (mostrar, gravar, enviar), sem devolver resultado. É o que o forEach recebe.

import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;

public class Main {
    public static void main(String[] args) {
        Consumer<String> chamar = nome -> System.out.print("Chamando " + nome);
        Consumer<String> pular = nome -> System.out.println("...");
        BiConsumer<String, Double> boletim = (nome, nota) -> System.out.println(nome + ": " + nota);

        chamar.accept("Ana");
        System.out.println();
        Consumer<String> chamarEPular = chamar.andThen(pular);
        List.of("Bruno", "Carla").forEach(chamarEPular);
        boletim.accept("Duda", 8.0);
    }
}
$ javac Main.java
$ java Main
Chamando Ana
Chamando Bruno...           « andThen: primeiro chamar, depois pular
Chamando Carla...
Duda: 8.0

accept = aceitar ("receba este valor"). c1.andThen(c2) monta um consumidor que faz c1 e depois c2 com o mesmo valor. BiConsumer recebe dois valores; é o que o forEach de um Map usa.

Supplier: fornecer (sem receber nada)

Um fornecedor não recebe nada e entrega um valor cada vez que você chama get. Serve para "fábricas" e para adiar um cálculo até ele ser necessário (você vai vê-lo no orElseGet do Optional).

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        Random dado = new Random(7);
        Supplier<Integer> rolar = () -> dado.nextInt(6) + 1;
        Supplier<List<String>> novaLista = () -> new ArrayList<>();
        Supplier<String> saudacao = () -> "Bom dia, turma!";

        System.out.println(rolar.get() + " " + rolar.get() + " " + rolar.get());
        List<String> l = novaLista.get();
        l.add("Ana");
        System.out.println(l + " " + saudacao.get());
    }
}
$ javac Main.java
$ java Main
5 3 4                        « cada get() rola o dado de novo (semente 7: sempre 5 3 4)
[Ana] Bom dia, turma!

UnaryOperator e BinaryOperator: mesmo tipo dentro e fora

São atalhos: UnaryOperator<T> é uma Function<T, T>, e BinaryOperator<T> é uma BiFunction<T, T, T>. "Unário" = de um operando (como o - em -x); "binário" = de dois (como o + em a + b).

import java.util.ArrayList;
import java.util.List;
import java.util.function.BinaryOperator;
import java.util.function.UnaryOperator;

public class Main {
    public static void main(String[] args) {
        UnaryOperator<String> gritar = s -> s.toUpperCase() + "!";
        BinaryOperator<Integer> maior = (a, b) -> a > b ? a : b;
        BinaryOperator<String> juntar = (a, b) -> a + " e " + b;

        System.out.println(gritar.apply("silêncio"));
        System.out.println(maior.apply(7, 10));
        System.out.println(juntar.apply("Ana", "Bruno"));

        List<String> nomes = new ArrayList<>(List.of("ana", "edu"));
        nomes.replaceAll(gritar);
        System.out.println(nomes);
    }
}
$ javac Main.java
$ java Main
SILÊNCIO!
10
Ana e Bruno
[ANA!, EDU!]                « replaceAll recebe um UnaryOperator

O BinaryOperator é a peça do reduce dos streams ("junte todos os elementos dois a dois"), que você vai ver em Streams.


Compor funções: andThen e compose

Funções podem ser encadeadas como uma linha de montagem: a saída de uma vira a entrada da próxima.

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<Integer, Integer> dobro = x -> x * 2;
        Function<Integer, Integer> maisUm = x -> x + 1;

        System.out.println("dobro.andThen(maisUm).apply(5): " + dobro.andThen(maisUm).apply(5));
        System.out.println("dobro.compose(maisUm).apply(5): " + dobro.compose(maisUm).apply(5));

        Function<String, String> limpar = s -> s.trim();
        Function<String, String> etiqueta = limpar
                .andThen(s -> s.toUpperCase())
                .andThen(s -> "[" + s + "]");
        System.out.println(etiqueta.apply("   caderno  "));
    }
}
$ javac Main.java
$ java Main
dobro.andThen(maisUm).apply(5): 11      « primeiro dobro (10), depois maisUm (11)
dobro.compose(maisUm).apply(5): 12      « primeiro maisUm (6), depois dobro (12)
[CADERNO]
ComposiçãoOrdemLê-se
f.andThen(g)f, depois g"faça f e então g" (a ordem da escrita)
f.compose(g)g, depois f"f aplicada ao resultado de g" (como na matemática, f(g(x)))

Na dúvida, use andThen: a ordem de execução é a ordem em que você lê. Function.identity() é a função que devolve o próprio valor (x -> x), útil como "peça neutra" e muito usada com Collectors.


Métodos que recebem funções

O poder de verdade aparece quando você escreve métodos que recebem uma interface funcional. O método cuida do "como percorrer"; quem chama decide "o que fazer":

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;

public class Main {
    static List<String> filtrar(List<String> lista, Predicate<String> regra) {
        List<String> resultado = new ArrayList<>();
        for (String s : lista) {
            if (regra.test(s)) {
                resultado.add(s);
            }
        }
        return resultado;
    }

    static List<Integer> transformar(List<String> lista, Function<String, Integer> f) {
        List<Integer> resultado = new ArrayList<>();
        for (String s : lista) {
            resultado.add(f.apply(s));
        }
        return resultado;
    }

    public static void main(String[] args) {
        List<String> nomes = List.of("Ana", "Bruno", "Beatriz", "Carla");
        System.out.println(filtrar(nomes, n -> n.startsWith("B")));
        System.out.println(filtrar(nomes, n -> n.length() <= 4));
        System.out.println(transformar(nomes, n -> n.length()));
    }
}
$ javac Main.java
$ java Main
[Bruno, Beatriz]
[Ana]
[3, 5, 7, 5]

Um único filtrar, infinitas regras. Esses dois métodos são, em miniatura, o filter e o map dos Streams.


Versões para tipos primitivos

Como generics não aceitam int e double, um Predicate<Integer> embrulha cada número num Integer. Para evitar isso em cálculos pesados, existem versões especializadas, com o tipo no nome:

InterfaceFormatoMétodo
IntPredicate, DoublePredicate, LongPredicateint → booleantest
IntFunction<R>int → Rapply
ToIntFunction<T>, ToDoubleFunction<T>T → int / doubleapplyAsInt / applyAsDouble
IntUnaryOperatorint → intapplyAsInt
IntBinaryOperator(int, int) → intapplyAsInt
IntConsumer, IntSupplier, BooleanSupplier…accept, getAsInt, getAsBoolean

Você vai encontrá-las nos streams de números (IntStream). Para começar, as genéricas bastam.


Usar uma pronta ou criar a sua?

SituaçãoUse
O formato é um dos da tabela (transformar, testar, consumir, fornecer…)A interface pronta. Todo programador Java reconhece Predicate<Aluno>.
A função tem 3 ou mais parâmetrosA sua (não existe TriFunction na biblioteca).
O nome ajuda a entender o domínio (Desconto, RegraDeAprovacao)A sua, com @FunctionalInterface.
O método precisa lançar uma exceção checked (como IOException)A sua, com throws no método (as prontas não declaram exceções; veja Checked × Unchecked).

Quando dá errado

"cannot find symbol … method apply(int) … Predicate": o método errado

Este código tem um erro de propósito.

import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> aprovado = n -> n >= 7;
        System.out.println(aprovado.apply(8));
    }
}
$ javac Main.java
Main.java:6: error: cannot find symbol
        System.out.println(aprovado.apply(8));
                                   ^
  symbol:   method apply(int)
  location: variable aprovado of type Predicate<Integer>
1 error

O que quer dizer: Predicate não tem apply: o método dele é test. Cada interface tem o seu nome de método.

Como consertar: Function/UnaryOperator/BinaryOperator → apply; Predicate → test; Consumer → accept; Supplier → get.

"Unexpected @FunctionalInterface annotation"

O que aconteceu: a interface anotada tem mais de um método abstrato (ou nenhum).

Como consertar: deixe um só método abstrato; os outros podem virar default (com corpo). Ou, se ela não é para lambdas, tire a anotação.

"unexpected type … required: reference … found: int"

O que aconteceu: você escreveu Function<int, int>. Generics não aceitam primitivos (veja Generics).

Como consertar: Function<Integer, Integer>, ou a versão primitiva IntUnaryOperator.


Referência

InterfaceMétodoComposiçãoUsada em
Function<T, R>R apply(T)andThen, compose, identity()stream.map, Comparator.comparing
BiFunction<T, U, R>R apply(T, U)andThenMap.merge, Map.replaceAll
Predicate<T>boolean test(T)and, or, negate, Predicate.notremoveIf, stream.filter
Consumer<T>void accept(T)andThenforEach
BiConsumer<T, U>void accept(T, U)andThenMap.forEach
Supplier<T>T get()—Optional.orElseGet, Collectors.toCollection
UnaryOperator<T>T apply(T)(de Function)List.replaceAll
BinaryOperator<T>T apply(T, T)minBy, maxBystream.reduce

Exercícios

1. O método do Predicate

Complete para testar se 8 passa no predicado aprovado:

boolean ok = aprovado.;

2. A interface certa

Complete o tipo de uma função que recebe um nome (String) e devolve o tamanho (Integer):

tamanho = s -> s.length();

3. Fornecedor

Complete para obter o valor do Supplier<String> saudacao:

String s = saudacao.;

4. Qual interface descreve "recebe um valor e não devolve nada"?

5. f = x -> x + 1, g = x -> x * 10. Quanto dá f.andThen(g).apply(2)?

6. O que p.and(q) faz?

Missão final: a loja da Ana dá 10% de desconto e depois soma R$ 15 de frete. Monte precoFinal compondo as duas funções com andThen (na ordem certa!). Para um produto de 100.0, a saída deve ser 105.0.

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<Double, Double> desconto = p -> p * 0.9;
        Function<Double, Double> frete = p -> p + 15;
        Function<Double, Double> precoFinal = frete.andThen(desconto);
        System.out.println(precoFinal.apply(100.0));
    }
}
import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<Double, Double> desconto = p -> p * 0.9;
        Function<Double, Double> frete = p -> p + 15;
        Function<Double, Double> precoFinal = desconto.andThen(frete);
        System.out.println(precoFinal.apply(100.0));
    }
}

Resumo

  • Interface funcional = um único método abstrato. @FunctionalInterface pede ao compilador para garantir isso.
  • java.util.function traz as prontas: Function (apply), BiFunction, Predicate (test), Consumer (accept), Supplier (get), UnaryOperator, BinaryOperator.
  • Compor: andThen/compose (funções), and/or/negate (predicados), andThen (consumidores).
  • Métodos que recebem interfaces funcionais separam o "como" (o método) do "o quê" (a lambda).
  • Há versões para primitivos: IntPredicate, ToIntFunction…
  • Crie a sua quando nenhum formato pronto servir ou quando um nome do domínio ajudar.

Próxima aula: Referências de Métodos, o atalho :: para lambdas que só chamam um método.