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.
- O que é uma interface funcional e o que faz a anotação
@FunctionalInterface. FunctioneBiFunction(transformar),Predicate(testar),Consumer(usar),Supplier(fornecer).UnaryOperatoreBinaryOperator(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):
| Interface | Recebe | Devolve | Método | Ideia | Exemplo |
|---|---|---|---|---|---|
Function<T, R> | um T | um R | apply | transformar | s -> s.length() |
BiFunction<T, U, R> | T e U | um R | apply | combinar dois | (preco, qtd) -> preco * qtd |
Predicate<T> | um T | boolean | test | testar, filtrar | n -> n >= 7 |
Consumer<T> | um T | nada | accept | usar, fazer algo | s -> System.out.println(s) |
Supplier<T> | nada | um T | get | fornecer, fabricar | () -> new ArrayList<>() |
UnaryOperator<T> | um T | um T | apply | transformar sem mudar o tipo | s -> s.toUpperCase() |
BinaryOperator<T> | dois T | um T | apply | juntar 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 umInteger(quantidade), devolve umDouble. 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étodo | Equivale a | Devolve |
|---|---|---|
p.test(x) | executar o teste | boolean |
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ção | Ordem | Lê-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:
| Interface | Formato | Método |
|---|---|---|
IntPredicate, DoublePredicate, LongPredicate | int → boolean | test |
IntFunction<R> | int → R | apply |
ToIntFunction<T>, ToDoubleFunction<T> | T → int / double | applyAsInt / applyAsDouble |
IntUnaryOperator | int → int | applyAsInt |
IntBinaryOperator | (int, int) → int | applyAsInt |
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ção | Use |
|---|---|
| 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âmetros | A 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
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.
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.
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
| Interface | Método | Composição | Usada em |
|---|---|---|---|
Function<T, R> | R apply(T) | andThen, compose, identity() | stream.map, Comparator.comparing |
BiFunction<T, U, R> | R apply(T, U) | andThen | Map.merge, Map.replaceAll |
Predicate<T> | boolean test(T) | and, or, negate, Predicate.not | removeIf, stream.filter |
Consumer<T> | void accept(T) | andThen | forEach |
BiConsumer<T, U> | void accept(T, U) | andThen | Map.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, maxBy | stream.reduce |
Exercícios
1. O método do Predicate
Complete para testar se 8 passa no predicado aprovado:
2. A interface certa
Complete o tipo de uma função que recebe um nome (String) e devolve o tamanho (Integer):
3. Fornecedor
Complete para obter o valor do Supplier<String> 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.
@FunctionalInterfacepede ao compilador para garantir isso. java.util.functiontraz 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.