Interfaces Funcionais Integradas do Java
O pacote java.util.function — Function, Predicate, Consumer, Supplier e suas variantes especializadas.
O pacote java.util.function foi incluído no Java 8 para fornecer ao JDK — e ao seu código — um vocabulário compartilhado para lambdas. Sem ele, cada método que aceitasse uma função precisaria definir sua própria interface pontual (StringMapper, IntToBool, RowHandler, …), e lambdas definidas para uma não poderiam ser reutilizadas para outra. O pacote resolve isso com 43 pequenas interfaces que cobrem os formatos que surgem repetidamente: "receber uma coisa e retornar outra", "receber uma coisa e decidir sim ou não", "receber uma coisa e fazer algo", "me dar uma coisa".
Se você aprender apenas quatro interfaces deste pacote, aprenda Function, Predicate, Consumer e Supplier. Quase tudo mais é uma variante de uma delas — versões de dois argumentos, especializações primitivas para evitar boxing, ou auxiliares de composição.
As quatro principais
Function<T, R> f = t -> ...; // takes a T, returns an R — r = f.apply(t)
Predicate<T> p = t -> ...; // takes a T, returns a boolean — boolean b = p.test(t)
Consumer<T> c = t -> { ... }; // takes a T, returns nothing — c.accept(t)
Supplier<T> s = () -> ...; // takes nothing, returns a T — t = s.get()Cada uma é anotada com @FunctionalInterface e possui um método abstrato de uma palavra (apply, test, accept, get). Raramente você chamará esses métodos diretamente quando streams estão envolvidos — stream().filter(predicate).map(function).forEach(consumer) faz as chamadas por você — mas conhecer o nome do método importa quando você escreve código que recebe um Function<T, R> como parâmetro e precisa invocá-lo.
Os formatos se mapeiam em perguntas comuns:
| Pergunta | Interface |
|---|---|
| "Transformar um X em um Y?" | Function<X, Y> |
| "Este X é bom?" | Predicate<X> |
| "Fazer algo com este X" | Consumer<X> |
| "Me dê um X" | Supplier<X> |
Variantes de dois argumentos
Quando a operação precisa de dois argumentos, adicione o prefixo Bi:
BiFunction<T, U, R> f = (t, u) -> ...; // two ins, one out — apply
BiPredicate<T, U> p = (t, u) -> ...; // two ins, a boolean — test
BiConsumer<T, U> c = (t, u) -> { ... }; // two ins, no out — acceptNão existe BiSupplier — Supplier não recebe argumentos por definição, então um "supplier de dois argumentos" seria apenas uma BiFunction.
As variantes Bi são exatamente o que Map.forEach((k, v) -> ...), Map.merge e Map.compute esperam:
Map<String, Integer> scores = new HashMap<>();
scores.forEach((name, score) -> System.out.println(name + "=" + score)); // BiConsumer
scores.merge("alice", 1, Integer::sum); // BinaryOperator<Integer>BinaryOperator<T> é uma BiFunction<T, T, T> — mesmo tipo para as duas entradas e a saída. UnaryOperator<T> é similarmente uma Function<T, T>.
Especializações primitivas — evitando o custo de boxing
Function<Integer, Integer> funciona, mas toda chamada encapsula a entrada e encapsula o resultado em objetos. Em um loop intensivo, isso tem um custo real. O pacote, portanto, oferece versões especializadas para primitivos:
IntFunction<R> f = i -> ...; // int in, R out
IntPredicate p = i -> ...; // int in, boolean out
IntConsumer c = i -> { ... }; // int in, void
IntSupplier s = () -> 42; // void in, int out
IntUnaryOperator u = i -> i * 2; // int in, int out
IntBinaryOperator b = (a, c2) -> a + c2;
ToIntFunction<T> f1 = t -> t.hashCode(); // T in, int out
ToIntBiFunction<T, U> f2 = (t, u) -> t.hashCode() + u.hashCode();
IntToLongFunction f3 = i -> (long) i * i; // int in, long out
IntToDoubleFunction f4 = i -> Math.sqrt(i);A mesma família existe para Long e Double. A convenção de nomenclatura lê como uma frase:
IntX— opera sobre umint.ToIntX— produz umint.IntToLongX—intde entrada,longde saída.
Em código com streams, mapToInt(...) retorna um IntStream, cujas operações terminais (sum, average, min, max) retornam primitivos sem boxing — o que é um dos maiores ganhos práticos das variantes primitivas.
Composição integrada nas interfaces
A maioria das interfaces vem com métodos default que permitem compor sem escrever novas lambdas:
// Function: andThen (left-to-right), compose (right-to-left)
Function<String, String> trim = String::trim;
Function<String, Integer> len = String::length;
Function<String, Integer> trimLen = trim.andThen(len); // trim, then length
Function<String, Integer> sameThing = len.compose(trim); // length applied after trim
// Predicate: and / or / negate
Predicate<String> notNull = Objects::nonNull;
Predicate<String> notBlank = s -> !s.trim().isEmpty();
Predicate<String> useful = notNull.and(notBlank);
Predicate<String> blank = notBlank.negate();
// Consumer: andThen (run two consumers in sequence)
Consumer<String> log = System.out::println;
Consumer<String> save = s -> writeToFile(s);
Consumer<String> both = log.andThen(save);
// Comparator (in java.util, not java.util.function, but the same idea):
Comparator<Person> byName = Comparator.comparing(Person::name);
Comparator<Person> ordered = byName.thenComparing(Person::age);Existe também uma fábrica estática útil: Predicate.not(p) é um atalho para p.negate() e lê de forma mais natural em um ponto de chamada:
list.removeIf(Predicate.not(String::isBlank)); // remove all blank stringsFunction.identity e Predicate.isEqual — os pequenos estáticos úteis
Dois métodos de fábrica que você verá em código com streams e deve reconhecer:
Function<T, T> id = Function.identity(); // t -> t — useful as a no-op map
Predicate<Object> isFoo = Predicate.isEqual("foo"); // o -> Objects.equals(o, "foo")Function.identity() é mais frequentemente usado como mapeador de chave ou valor em Collectors.toMap:
Map<String, Person> byName = people.stream()
.collect(Collectors.toMap(Person::name, Function.identity()));Predicate.isEqual raramente é mais curto do que s -> s.equals("foo"), mas compara com segurança para null usando Objects.equals, o que importa quando o stream pode conter null.
Um exemplo prático: as quatro principais, composição e especialização primitiva
O programa abaixo usa Function, Predicate, Consumer e Supplier, compõe alguns deles e contrasta um Function<Integer, Integer> (com boxing) com um IntUnaryOperator (primitivo) somando uma pequena lista.
O que extrair da execução:
- As quatro interfaces principais mapeiam de forma limpa em quatro tipos de trabalho: transformar (
Function), testar (Predicate), agir (Consumer), produzir (Supplier). Os nomes dos seus métodos abstratos (apply,test,accept,get) valem a pena memorizar. trim.andThen(length)enotNull.and(notBlank)construíram novos valores a partir dos antigos sem declarações de métodos auxiliares. Essa é a álgebra de composição que as interfaces carregam como métodosdefault.- O
Function<Integer, Integer>com boxing é significativamente mais lento que oIntUnaryOperatorprimitivo porque cada chamada aloca dois objetosInteger. Em caminhos críticos — pipelines de stream que processam milhões de valores — as especializações primitivas valem o esforço. Predicate.not(notBlank)lê de forma mais natural do quenotBlank.negate()em um ponto de chamadaremoveIf. Ambos compilam para a mesma coisa.
O que vem a seguir
Você agora conhece o vocabulário padrão. A questão restante de ergonomia de lambdas é "quando o corpo da lambda apenas delega a um método existente, posso escrever de forma mais curta?" Sim — com referências a métodos. O próximo capítulo, Referências a Métodos em Java, cobre o operador :: e suas quatro formas (estático, instância ligada, instância não ligada, construtor), e explica quando uma referência a método é mais clara que uma lambda e quando não é.