W3docs

Interfaz funcional Function de Java

Transforma un valor de un tipo en otro en Java con la interfaz Function y los métodos andThen/compose.

Function<T, R> es la interfaz funcional para la pregunta "convierte este T en un R" — una entrada, una salida, sin efectos secundarios esperados. Es la forma que acepta Stream.map, la que acepta Optional.map, la que acepta Map.computeIfAbsent, y la que acepta cualquier método del JDK que diga "transforma esto en aquello". Un único método abstracto, tres o cuatro métodos predeterminados útiles, y un pequeño álgebra (andThen, compose, identity) para encadenar transformaciones sin escribir lambdas intermedias.

La interfaz

@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) es el SAM (método abstracto único). Toda lambda o referencia a método que termine en una posición Function<T, R> lo implementa.

Function<String, Integer> length = String::length;
int n = length.apply("hello");                  // 5

Por lo general dejarás que stream.map(length) o optional.map(length) llamen a apply por ti. Conocer el nombre del método importa cuando escribes código que acepta una Function<T, R> y necesita invocarla una vez.

andThen y compose — dos formas de encadenar

Los dos métodos predeterminados construyen una nueva Function encadenando el receptor con otra. Solo difieren en la dirección:

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 construyen el mismo pipeline s -> length(trim(s)). La diferencia está en cuál se lee mejor en el lugar de la llamada:

  • andThen se lee de izquierda a derecha, en el mismo orden en que fluyen los datos. trim.andThen(length).andThen(asString) significa "trim, luego length, luego asString."
  • compose se lee de derecha a izquierda, tal como se escribe la composición matemática: f ∘ g significa "aplicar g primero, luego f." length.compose(trim) significa "length después de trim."

En el código de aplicación, andThen es casi siempre la opción más clara — el código se lee de arriba a abajo, de izquierda a derecha, y un pipeline de izquierda a derecha coincide con eso. compose es útil cuando tienes una función final y quieres añadir antes un preprocesamiento sin reescribir la cadena.

Ambos son lazy en el sentido de que no ejecutan nada en el momento de la composición; simplemente producen una nueva Function cuyo apply llama a las subyacentes en el orden correcto.

Function.identity() — la transformación nula

Function<T, T> id = Function.identity();      // t -> t

identity() devuelve la misma instancia en cada llamada (una lambda singleton), por lo que tiene coste de asignación cero. El único lugar donde resulta útil es como mapeador de clave o valor en Collectors.toMap, donde necesitas pasar una Function incluso cuando el valor es "el elemento mismo":

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

Sin Function.identity() escribirías p -> p, lo que asigna una nueva lambda en cada llamada y resulta menos legible.

Un matiz sutil: identity() solo funciona cuando los tipos de entrada y salida son iguales. En el momento en que un genérico se amplía (Function<? super T, ? extends R>), el compilador puede obligarte a escribir una lambda explícita de nuevo. Es un caso extremo, pero vale la pena conocerlo cuando la inferencia de tipos genéricos se queja.

Function<T, R> frente a UnaryOperator<T>

UnaryOperator<T> es la especialización para el caso en que el tipo de entrada y el de salida son el mismo:

UnaryOperator<String> upper = String::toUpperCase;       // String -> String
Function<String, String> sameShape = String::toUpperCase;

Ambos son instancias válidas de Function<String, String>UnaryOperator<T> extiende Function<T, T>. La diferencia está a nivel de API: List.replaceAll, Map.replaceAll y Comparator.thenComparing(UnaryOperator) declaran UnaryOperator<T> porque "reemplaza cada elemento por un valor transformado del mismo tipo" es exactamente esa forma. Pasa una referencia a método y el compilador elige la correcta.

BiFunction<T, U, R> — dos entradas

La forma de dos argumentos:

BiFunction<String, Integer, String> repeat = String::repeat;
String s = repeat.apply("ab", 3);             // "ababab"

BiFunction tiene el mismo andThen pero no tiene compose — la asimetría es deliberada, porque preprocesar una función de dos argumentos requeriría dos parámetros para compose.

El JDK usa BiFunction<K, V, V> para Map.merge y BiFunction<K, V, V_NEW> para Map.compute. BinaryOperator<T> es el caso especial en el que los tres parámetros de tipo son T (entrada, entrada y salida son iguales) — cubierto en el capítulo de BinaryOperator.

Especializaciones primitivas — tres familias

Function<Integer, String> encapsula el int en cada llamada. El paquete incluye tres familias para evitarlo:

// 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;

La nomenclatura se lee como una frase:

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

Stream.mapToInt(ToIntFunction) es el puente desde un Stream<T> encapsulado hacia un IntStream. Una vez en un IntStream, cada transformación usa IntUnaryOperator o IntToLongFunction — y el coste de encapsulamiento se mantiene en cero.

Un ejemplo completo: composición, identity y una especialización primitiva

El programa a continuación construye dos Functions, las compone con andThen y compose para demostrar que son equivalentes, usa Function.identity() dentro de un Collectors.toMap, y contrasta una Function<Integer, Integer> encapsulada con una IntUnaryOperator primitiva sobre una carga de trabajo lo suficientemente grande como para notar el coste de encapsulamiento.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • trim.andThen(upper) y upper.compose(trim) produjeron el mismo String a partir de la misma entrada. Solo difieren en qué nombre se lee de forma natural donde lo escribes — andThen coincide con el flujo de datos de izquierda a derecha, compose coincide con la notación matemática "f después de g".
  • La cadena más larga trim.andThen(upper).andThen(length) cambió el tipo de salida de String a Integer a lo largo del camino. El pipeline se compone de forma segura para los tipos; el compilador rastreó String -> String -> String -> Integer por ti.
  • Function.identity() encajó en Collectors.toMap(Person::name, Function.identity()) como mapeador de valores. La lambda p -> p habría funcionado, pero identity() es la forma singleton sin asignación y se lee como la intención ("el valor es la persona").
  • La Function<Integer, Integer> encapsulada paga el coste de dos encapsulamientos de Integer en cada llamada; la IntUnaryOperator primitiva no paga nada. Una sola ejecución calentada puede mostrar tiempos similares — el JIT es bueno eliminando objetos de vida corta — pero bajo presión real de asignación (montones grandes, GC concurrente, valores que escapan) la variante primitiva es la que resiste. Úsala en pipelines críticos que procesen millones de valores.
  • BiFunction.andThen(Function) encadenó una función de dos argumentos con un seguimiento de un argumento. No existe BiFunction.compose — preprocesar dos entradas requeriría dos argumentos para compose, lo que la API evita deliberadamente.

Qué sigue

Function<T, R> y Predicate<T> son formas puras — entrada, salida, sin efectos secundarios esperados. El próximo capítulo, Java Consumer y Supplier, cubre las dos interfaces que salen de esa pureza: Consumer<T> toma una entrada y no produce nada (un efecto secundario — imprimir, registrar, almacenar), y Supplier<T> no toma nada y produce una salida (valor predeterminado lazy, fábrica, aleatoriedad). Completan la taxonomía de cuatro esquinas que viste en el resumen de interfaces integradas.

Práctica

Práctica
Tienes `Function<String, String> trim = String::trim;` y `Function<String, Integer> length = String::length;`. Quieres una `Function<String, Integer>` que recorte primero y luego tome la longitud. ¿Qué expresión la construye de forma más natural?
Tienes `Function<String, String> trim = String::trim;` y `Function<String, Integer> length = String::length;`. Quieres una `Function<String, Integer>` que recorte primero y luego tome la longitud. ¿Qué expresión la construye de forma más natural?
Was this page helpful?