Interface Function do Java
Transforme um valor de um tipo em outro no Java com a interface Function e os métodos andThen/compose.
Function<T, R> é a interface funcional para a pergunta "transforme este T em um R" — uma entrada, uma saída, sem efeitos colaterais esperados. É a forma que Stream.map aceita, a forma que Optional.map aceita, a forma que Map.computeIfAbsent aceita, e a forma que todo método do JDK que diz "transforme isto em aquilo" recebe. Um único método abstrato, três ou quatro métodos padrão úteis, e uma pequena álgebra (andThen, compose, identity) para encadear transformações sem escrever lambdas intermediárias.
A interface
@FunctionalInterface
public interface Function<T, R> {
R apply(T t); // the only abstract method
default <V> Function<V, R> compose(Function<? super V, ? extends T> before);
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after);
static <T> Function<T, T> identity();
}apply(T) é o SAM (single abstract method). Todo lambda ou referência de método que termina em uma posição Function<T, R> o implementa.
Function<String, Integer> length = String::length;
int n = length.apply("hello"); // 5Normalmente você deixará stream.map(length) ou optional.map(length) chamar apply por você. Conhecer o nome do método importa quando você escreve código que aceita um Function<T, R> e precisa chamá-lo uma vez.
andThen e compose — duas formas de encadear
Os dois métodos padrão constroem um novo Function encadeando o receptor com outro. Eles diferem apenas na direção:
Function<String, String> trim = String::trim;
Function<String, Integer> length = String::length;
Function<String, Integer> trimThenLength = trim.andThen(length); // f.andThen(g): g(f(x))
Function<String, Integer> sameThing = length.compose(trim); // g.compose(f): g(f(x))Ambos constroem o mesmo pipeline s -> length(trim(s)). A diferença é qual deles lê melhor no ponto de chamada:
andThenlê da esquerda para a direita, na mesma ordem em que os dados fluem.trim.andThen(length).andThen(asString)é "trim, depois length, depois asString."composelê da direita para a esquerda, da forma como a composição matemática é escrita:f ∘ gsignifica "apliquegprimeiro, depoisf."length.compose(trim)é "length após trim."
No código de aplicação, andThen é quase sempre a escolha mais clara — o código lê de cima para baixo, da esquerda para a direita, e um pipeline da esquerda para a direita combina com isso. compose é útil quando você tem uma função final e quer antepor pré-processamento sem reescrever a cadeia.
Ambos são preguiçosos no sentido de que não executam nada no momento da composição; eles simplesmente produzem um novo Function cujo apply chama os subjacentes na ordem correta.
Function.identity() — a transformação que não faz nada
Function<T, T> id = Function.identity(); // t -> tidentity() retorna a mesma instância a cada chamada (um lambda singleton), portanto tem custo zero de alocação. O único lugar onde ele se destaca é como mapeador de chave ou valor em Collectors.toMap, onde você precisa passar um Function mesmo quando o valor é "o próprio elemento":
Map<String, Person> byName = people.stream()
.collect(Collectors.toMap(Person::name, Function.identity())); // key=name, value=personSem Function.identity() você escreveria p -> p, que aloca um novo lambda a cada chamada e lê pior.
Um ponto sutil: identity() só funciona quando os tipos de entrada e saída são os mesmos. No momento em que um genérico se amplia (Function<? super T, ? extends R>), o compilador pode forçar você a escrever um lambda novamente. É um caso extremo, mas vale saber quando a inferência de tipos reclama.
Function<T, R> versus UnaryOperator<T>
UnaryOperator<T> é a especialização para o caso em que entrada e saída são do mesmo tipo:
UnaryOperator<String> upper = String::toUpperCase; // String -> String
Function<String, String> sameShape = String::toUpperCase;Ambas são instâncias válidas de Function<String, String> — UnaryOperator<T> estende Function<T, T>. A diferença está no nível da API: List.replaceAll, Map.replaceAll e Comparator.thenComparing(UnaryOperator) declaram UnaryOperator<T> porque "substituir cada elemento por um valor transformado do mesmo tipo" é exatamente essa forma. Passe uma referência de método e o compilador escolhe a correta.
BiFunction<T, U, R> — duas entradas
A forma com dois argumentos:
BiFunction<String, Integer, String> repeat = String::repeat;
String s = repeat.apply("ab", 3); // "ababab"BiFunction tem o mesmo andThen, mas não tem compose — a assimetria é proposital, porque pré-processar uma função de dois argumentos precisaria de dois parâmetros compose.
O JDK usa BiFunction<K, V, V> para Map.merge e BiFunction<K, V, V_NEW> para Map.compute. BinaryOperator<T> é o caso especial em que todos os três parâmetros de tipo são T (entrada, entrada e saída iguais) — coberto no capítulo sobre BinaryOperator.
Especializações primitivas — três famílias
Function<Integer, String> encaixa o int em um objeto a cada chamada. O pacote traz três famílias para evitar isso:
// 1. Primitive in, object out — "IntFunction<R>"
IntFunction<String> fromInt = i -> "n=" + i;
// 2. Object in, primitive out — "ToIntFunction<T>"
ToIntFunction<String> strLen = String::length;
ToDoubleFunction<Item> price = Item::price;
// 3. Primitive in, primitive out — "IntToLongFunction", "IntUnaryOperator", etc.
IntToLongFunction square = i -> (long) i * i;
IntUnaryOperator doubleIt = i -> i * 2;
DoubleUnaryOperator halve = d -> d / 2.0;A nomenclatura lê como uma frase:
IntX— opera sobre umint.ToIntX— produz umint.IntToLongX—intna entrada,longna saída.
Stream.mapToInt(ToIntFunction) é a ponte de um Stream<T> com boxing para um IntStream. Uma vez em um IntStream, toda transformação usa IntUnaryOperator ou IntToLongFunction — e o custo do boxing permanece em zero.
Um exemplo prático: composição, identity e uma especialização primitiva
O programa abaixo constrói dois Functions, os compõe com andThen e compose para mostrar que são equivalentes, usa Function.identity() dentro de Collectors.toMap, e contrasta um Function<Integer, Integer> com boxing com um IntUnaryOperator primitivo em uma carga de trabalho grande o suficiente para sentir o custo do boxing.
O que observar na execução:
trim.andThen(upper)eupper.compose(trim)produziram a mesmaStringa partir da mesma entrada. Diferem apenas em qual nome lê naturalmente onde você o escreve —andThencombina com o fluxo de dados da esquerda para a direita,composecombina com a notação matemática "f após g".- A cadeia mais longa
trim.andThen(upper).andThen(length)mudou o tipo de saída deStringparaIntegerao longo do caminho. O pipeline compõe com segurança de tipos; o compilador rastreouString -> String -> String -> Integerpor você. Function.identity()encaixou emCollectors.toMap(Person::name, Function.identity())como o mapeador de valor. O lambdap -> pteria funcionado, masidentity()é a forma singleton sem alocação e lê como a intenção ("o valor é a pessoa").- O
Function<Integer, Integer>com boxing paga por dois boxings deIntegera cada chamada; oIntUnaryOperatorprimitivo não paga nada. Uma única execução aquecida pode mostrar tempos semelhantes — o JIT é bom em eliminar boxes de vida curta — mas sob pressão de alocação real (heaps grandes, GC concorrente, valores escapando) a variante primitiva é a que se mantém. Recorra a ela em pipelines quentes que processam milhões de valores. BiFunction.andThen(Function)encadeou uma função de dois argumentos com um acompanhamento de um argumento. Não háBiFunction.compose— pré-processar duas entradas precisaria de dois argumentoscompose, o que a API deliberadamente evita.
O que vem a seguir
Function<T, R> e Predicate<T> são ambas formas puras — entrada, saída, sem efeitos colaterais esperados. O próximo capítulo, Java Consumer e Supplier, cobre as duas interfaces que saem dessa pureza: Consumer<T> recebe uma entrada e não produz nada (um efeito colateral — imprimir, registrar, armazenar), e Supplier<T> não recebe nada e produz uma saída (padrão preguiçoso, fábrica, aleatoriedade). Elas completam a taxonomia dos quatro cantos que você viu na visão geral das interfaces integradas.