W3docs

Interfaces Funcionales Integradas de Java

El paquete java.util.function: Function, Predicate, Consumer, Supplier y sus variantes especializadas.

El paquete java.util.function se incluyó en Java 8 para proporcionar al JDK — y a tu código — un vocabulario compartido para las lambdas. Sin él, cada método que aceptara una función tendría que definir su propia interfaz puntual (StringMapper, IntToBool, RowHandler, …), y las lambdas definidas para una no podrían reutilizarse en otra. El paquete resuelve esto con 43 interfaces pequeñas que cubren las formas que aparecen una y otra vez: "toma algo, devuelve otra cosa", "toma algo, decide sí o no", "toma algo, haz algo con ello", "dame algo".

Si solo aprendes cuatro interfaces de este paquete, aprende Function, Predicate, Consumer y Supplier. Casi todo lo demás es una variante de una de ellas: versiones con dos argumentos, especializaciones primitivas para evitar el boxing, o helpers de composición.

Las cuatro principales

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 una está anotada con @FunctionalInterface y tiene un método abstracto de una sola palabra (apply, test, accept, get). Rara vez llamarás a esos métodos directamente cuando se trabaja con streams — stream().filter(predicate).map(function).forEach(consumer) realiza las llamadas por ti — pero conocer el nombre del método importa cuando escribes código que recibe un Function<T, R> como parámetro y necesita invocarlo.

Las formas se corresponden con preguntas comunes:

PreguntaInterfaz
"¿Transformar una X en una Y?"Function<X, Y>
"¿Esta X es válida?"Predicate<X>
"Hacer algo con esta X"Consumer<X>
"Dame una X"Supplier<X>

Variantes con dos argumentos

Cuando la operación necesita dos entradas, añade el prefijo 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                     — accept

No existe BiSupplierSupplier no toma argumentos por definición, por lo que un "supplier con dos argumentos" sería simplemente un BiFunction.

Las variantes Bi son exactamente lo que Map.forEach((k, v) -> ...), Map.merge y Map.compute esperan:

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> es un BiFunction<T, T, T> — el mismo tipo para ambas entradas y la salida. UnaryOperator<T> es similarmente un Function<T, T>.

Especializaciones primitivas — evitando el costo del boxing

Function<Integer, Integer> funciona, pero cada llamada hace boxing de la entrada y hace boxing del resultado. En un bucle muy ajustado, ese es un costo real. Por eso el paquete ofrece versiones 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);

La misma familia existe para Long y Double. La convención de nombres se lee como una oración:

  • IntX — opera sobre un int.
  • ToIntX — produce un int.
  • IntToLongXint de entrada, long de salida.

En el código de streams, mapToInt(...) devuelve un IntStream, cuyas operaciones terminales (sum, average, min, max) devuelven todos primitivos sin boxing — lo cual es uno de los mayores beneficios prácticos de las variantes primitivas.

Composición integrada en las interfaces

La mayoría de las interfaces vienen con métodos default que te permiten componer sin escribir nuevas 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);

También hay una útil fábrica estática: Predicate.not(p) es una forma abreviada de p.negate() y resulta más natural en el punto de llamada:

list.removeIf(Predicate.not(String::isBlank));    // remove all blank strings

Function.identity y Predicate.isEqual — los pequeños estáticos útiles

Dos métodos de fábrica que verás en el código de streams y que debes reconocer:

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() se usa con mayor frecuencia como mapeador de clave o valor en Collectors.toMap:

Map<String, Person> byName = people.stream()
    .collect(Collectors.toMap(Person::name, Function.identity()));

Predicate.isEqual raramente es más corto que s -> s.equals("foo"), pero compara de forma null-safe con Objects.equals, lo cual importa cuando el stream puede contener null.

Un ejemplo completo: las cuatro principales, composición y especialización primitiva

El programa a continuación usa Function, Predicate, Consumer y Supplier, compone algunos de ellos, y contrasta un Function<Integer, Integer> (con boxing) con un IntUnaryOperator (primitivo) sumando una lista pequeña.

java— editable, runs on the server

Lo que hay que extraer de la ejecución:

  • Las cuatro interfaces principales se mapean limpiamente en cuatro tipos de trabajo: transformar (Function), probar (Predicate), actuar (Consumer), producir (Supplier). Sus nombres de métodos abstractos (apply, test, accept, get) merecen memorizarse.
  • trim.andThen(length) y notNull.and(notBlank) construyeron nuevos valores a partir de los anteriores sin declaraciones de métodos auxiliares. Esa es el álgebra de composición que las interfaces llevan como métodos default.
  • El Function<Integer, Integer> con boxing es significativamente más lento que el IntUnaryOperator primitivo porque cada llamada asigna dos objetos Integer. En rutas de código con alto rendimiento — pipelines de streams que procesan millones de valores — las especializaciones primitivas justifican su existencia.
  • Predicate.not(notBlank) resulta más natural que notBlank.negate() en el punto de llamada de removeIf. Ambas compilan al mismo resultado.

Qué sigue

Ya has visto el vocabulario estándar. La pregunta ergonómica restante sobre las lambdas es "cuando el cuerpo de la lambda simplemente delega en un método existente, ¿puedo escribirlo de forma más breve?" Sí — con referencias a métodos. El siguiente capítulo, Referencias a Métodos en Java, cubre el operador :: y sus cuatro formas (estático, instancia vinculada, instancia no vinculada, constructor), y explica cuándo una referencia a método es más clara que una lambda y cuándo es lo contrario.

Práctica

Práctica
Un método declara `void each(Consumer<String> action)`. ¿Cuáles de los siguientes son argumentos válidos?
Un método declara `void each(Consumer<String> action)`. ¿Cuáles de los siguientes son argumentos válidos?
Was this page helpful?