Java

Tutorial Java: Interfaz funcional Function

Aprende la interfaz funcional Function en Java 8, su método apply y composición con andThen y compose para programación funcional.

Aprende Java y certifícate

Definición y propósito de la interfaz Function

La interfaz Function<T, R> es uno de los pilares fundamentales de la programación funcional en Java, introducida en Java 8 como parte del paquete java.util.function. Esta interfaz representa una operación que acepta un argumento de tipo T y produce un resultado de tipo R, permitiendo transformar datos de un tipo a otro de manera concisa y expresiva.

La definición básica de la interfaz Function es:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
    
    // Métodos default para composición
    // ...
}

Donde:

  • T es el tipo del parámetro de entrada
  • R es el tipo del resultado
  • La anotación @FunctionalInterface indica que esta interfaz está diseñada para ser implementada mediante expresiones lambda

El propósito principal de Function es proporcionar una forma estandarizada de representar transformaciones de datos, permitiendo:

  • Transformar un valor de un tipo a otro
  • Encapsular lógica de transformación como objetos de primera clase
  • Pasar comportamiento como argumento a métodos
  • Componer múltiples transformaciones en una sola operación

Casos de uso comunes

Function se utiliza en numerosos escenarios donde necesitamos transformar datos:

  • Mapeo de valores: Convertir elementos de una colección a otro formato
List<String> nombres = Arrays.asList("Ana", "Juan", "Carlos");
Function<String, Integer> obtenerLongitud = nombre -> nombre.length();

// Aplicando la función a cada elemento
List<Integer> longitudes = new ArrayList<>();
for (String nombre : nombres) {
    longitudes.add(obtenerLongitud.apply(nombre));
}
// longitudes contiene [3, 4, 6]
  • Transformación de objetos: Convertir objetos de dominio a DTOs (Data Transfer Objects)
Function<Usuario, UsuarioDTO> convertirADTO = usuario -> {
    UsuarioDTO dto = new UsuarioDTO();
    dto.setId(usuario.getId());
    dto.setNombreCompleto(usuario.getNombre() + " " + usuario.getApellido());
    return dto;
};

UsuarioDTO dto = convertirADTO.apply(usuarioActual);
  • Procesamiento condicional: Aplicar transformaciones basadas en condiciones
Function<Integer, String> clasificarNota = nota -> {
    if (nota >= 90) return "Sobresaliente";
    else if (nota >= 70) return "Notable";
    else if (nota >= 60) return "Bien";
    else if (nota >= 50) return "Aprobado";
    else return "Suspenso";
};

String resultado = clasificarNota.apply(85); // "Notable"

Implementación de Function

Existen varias formas de implementar la interfaz Function:

  • Mediante clases anónimas (estilo tradicional):
Function<Double, Long> redondear = new Function<Double, Long>() {
    @Override
    public Long apply(Double valor) {
        return Math.round(valor);
    }
};

Long resultado = redondear.apply(3.75); // 4
  • Mediante expresiones lambda (forma concisa):
Function<Double, Long> redondear = valor -> Math.round(valor);
Long resultado = redondear.apply(3.75); // 4
  • Mediante referencias a métodos:
Function<String, Integer> convertirAEntero = Integer::parseInt;
Integer numero = convertirAEntero.apply("123"); // 123

Function como parámetro de método

Una de las ventajas más importantes de Function es la capacidad de pasar comportamiento como parámetro:

public static <T, R> List<R> transformarLista(List<T> lista, Function<T, R> transformador) {
    List<R> resultado = new ArrayList<>();
    for (T elemento : lista) {
        resultado.add(transformador.apply(elemento));
    }
    return resultado;
}

// Uso del método
List<String> nombres = Arrays.asList("Ana", "Juan", "Carlos");
List<Integer> longitudes = transformarLista(nombres, nombre -> nombre.length());
List<String> mayusculas = transformarLista(nombres, nombre -> nombre.toUpperCase());

Este patrón es extremadamente flexible y permite reutilizar la lógica de procesamiento mientras se varía la transformación aplicada.

Funciones de identidad y constantes

Java proporciona algunos métodos de fábrica útiles para casos comunes:

// Función de identidad - devuelve el mismo valor que recibe
Function<String, String> identidad = Function.identity();
String resultado = identidad.apply("hola"); // "hola"

// Función constante - siempre devuelve el mismo valor
Function<Integer, String> constante = valor -> "Constante";
String salida = constante.apply(42); // "Constante"

La interfaz Function es la base para muchas otras interfaces funcionales especializadas en Java, como BiFunction, UnaryOperator y BinaryOperator, que veremos en secciones posteriores. Su diseño simple pero potente permite expresar transformaciones de datos de forma clara y concisa, facilitando la adopción de un estilo de programación más funcional en Java.

El método apply()

El método apply() es el núcleo funcional de la interfaz Function, siendo el único método abstracto que debe implementarse obligatoriamente. Este método encapsula la operación de transformación que convierte un valor de entrada en un resultado, posiblemente de un tipo diferente.

La firma del método es simple pero poderosa:

R apply(T t);

Donde:

  • T representa el tipo del parámetro de entrada
  • R representa el tipo del resultado devuelto
  • El método recibe un único argumento y debe retornar un valor

Comportamiento del método apply()

El método apply() actúa como un transformador que procesa el valor de entrada y produce un resultado basado en la lógica definida en su implementación. Esta transformación puede incluir:

  • Conversiones de tipo
  • Cálculos matemáticos
  • Extracción de propiedades
  • Formateo de datos
  • Cualquier operación que genere un resultado a partir de una entrada

Ejemplos prácticos de apply()

Veamos algunos ejemplos concretos que ilustran el uso del método apply() en diferentes contextos:

  • Transformación de tipos básicos:
Function<Integer, Double> dividirPorDos = numero -> numero / 2.0;
Double resultado = dividirPorDos.apply(10); // 5.0
  • Manipulación de cadenas:
Function<String, String> invertir = texto -> {
    StringBuilder sb = new StringBuilder(texto);
    return sb.reverse().toString();
};
String textoInvertido = invertir.apply("Java"); // "avaJ"
  • Extracción de información de objetos complejos:
class Producto {
    private String nombre;
    private double precio;
    
    // Constructor y getters
    public Producto(String nombre, double precio) {
        this.nombre = nombre;
        this.precio = precio;
    }
    
    public String getNombre() { return nombre; }
    public double getPrecio() { return precio; }
}

Function<Producto, String> obtenerNombre = producto -> producto.getNombre();
Function<Producto, Double> obtenerPrecioConIVA = producto -> producto.getPrecio() * 1.21;

Producto laptop = new Producto("Laptop XPS", 1200.0);
String nombre = obtenerNombre.apply(laptop); // "Laptop XPS"
Double precioFinal = obtenerPrecioConIVA.apply(laptop); // 1452.0
  • Validación y transformación condicional:
Function<String, Boolean> esEmailValido = email -> 
    email != null && email.matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$");

Boolean esValido = esEmailValido.apply("usuario@dominio.com"); // true

Encadenando operaciones con apply()

Aunque la composición formal se realiza con los métodos andThen() y compose() (que veremos en la siguiente sección), podemos encadenar llamadas a apply() de forma manual:

Function<String, Integer> contarCaracteres = s -> s.length();
Function<Integer, Boolean> esPar = num -> num % 2 == 0;

// Encadenamiento manual
Boolean resultado = esPar.apply(contarCaracteres.apply("Java")); // true (4 es par)

Uso de apply() con colecciones

El método apply() es especialmente útil cuando se trabaja con colecciones de datos:

List<String> nombres = Arrays.asList("Ana", "Juan", "María", "Carlos");
Function<String, String> primeraLetra = nombre -> nombre.substring(0, 1);

// Aplicando la función a cada elemento manualmente
List<String> iniciales = new ArrayList<>();
for (String nombre : nombres) {
    iniciales.add(primeraLetra.apply(nombre));
}
// iniciales contiene ["A", "J", "M", "C"]

Manejo de excepciones en apply()

El método apply() no declara excepciones verificadas, pero puede lanzar excepciones no verificadas. Es importante manejarlas adecuadamente:

Function<String, Integer> parseEntero = s -> {
    try {
        return Integer.parseInt(s);
    } catch (NumberFormatException e) {
        return 0; // Valor por defecto en caso de error
    }
};

Integer numero1 = parseEntero.apply("123"); // 123
Integer numero2 = parseEntero.apply("abc"); // 0 (valor por defecto)

Implementación de apply() en clases personalizadas

Podemos crear nuestras propias clases que implementen la interfaz Function:

class Formateador implements Function<Double, String> {
    private final String formato;
    
    public Formateador(String formato) {
        this.formato = formato;
    }
    
    @Override
    public String apply(Double valor) {
        return String.format(formato, valor);
    }
}

// Uso de nuestra implementación personalizada
Formateador formatoMoneda = new Formateador("$%.2f");
String precio = formatoMoneda.apply(29.99); // "$29.99"

Optimización del método apply()

Para funciones que se invocan frecuentemente, es importante considerar la eficiencia de la implementación:

// Implementación ineficiente (crea un nuevo objeto en cada llamada)
Function<Integer, List<Integer>> factoresIneficiente = n -> {
    List<Integer> factores = new ArrayList<>();
    for (int i = 1; i <= n; i++) {
        if (n % i == 0) factores.add(i);
    }
    return factores;
};

// Implementación más eficiente (usa un algoritmo optimizado)
Function<Integer, List<Integer>> factoresEficiente = n -> {
    List<Integer> factores = new ArrayList<>();
    for (int i = 1; i <= Math.sqrt(n); i++) {
        if (n % i == 0) {
            factores.add(i);
            if (i != n/i) factores.add(n/i);
        }
    }
    Collections.sort(factores);
    return factores;
};

El método apply() es la esencia de la interfaz Function, permitiendo expresar transformaciones de datos de manera clara y concisa. Su simplicidad oculta su gran potencial, especialmente cuando se combina con otras funciones mediante composición, como veremos en la siguiente sección.

Composición de funciones con andThen() y compose()

La composición de funciones es un concepto fundamental en programación funcional que permite combinar múltiples funciones para crear transformaciones más complejas. En Java, la interfaz Function proporciona dos métodos clave para este propósito: andThen() y compose().

Estos métodos permiten encadenar operaciones de manera declarativa, mejorando la legibilidad y modularidad del código. Veamos cómo funcionan:

El método andThen()

El método andThen() permite ejecutar una función después de otra, pasando el resultado de la primera como entrada a la segunda:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)

Donde:

  • T es el tipo de entrada de la función original
  • R es el tipo de salida de la función original
  • V es el tipo de salida de la función compuesta

El flujo de ejecución es: f.andThen(g) equivale a g(f(x)).

Ejemplo básico:

Function<Integer, Integer> duplicar = x -> x * 2;
Function<Integer, String> convertirATexto = x -> "Número: " + x;

// Primero duplica, luego convierte a texto
Function<Integer, String> duplicarYConvertir = duplicar.andThen(convertirATexto);

String resultado = duplicarYConvertir.apply(5); // "Número: 10"

El método compose()

El método compose() funciona en dirección opuesta a andThen(). Ejecuta primero la función proporcionada como argumento y luego aplica la función original al resultado:

default <V> Function<V, R> compose(Function<? super V, ? extends T> before)

Donde:

  • V es el tipo de entrada de la función compuesta
  • T es el tipo de entrada de la función original
  • R es el tipo de salida de la función original

El flujo de ejecución es: f.compose(g) equivale a f(g(x)).

Ejemplo básico:

Function<Integer, Integer> duplicar = x -> x * 2;
Function<String, Integer> obtenerLongitud = s -> s.length();

// Primero obtiene la longitud, luego duplica
Function<String, Integer> obtenerLongitudYDuplicar = duplicar.compose(obtenerLongitud);

Integer resultado = obtenerLongitudYDuplicar.apply("Java"); // 8 (longitud 4 * 2)

Diferencia entre andThen() y compose()

La principal diferencia entre estos métodos es el orden de ejecución:

// Con andThen: primero f, luego g
Function<T, V> andThenResult = f.andThen(g);  // g(f(x))

// Con compose: primero g, luego f
Function<V, R> composeResult = f.compose(g);  // f(g(x))

Esta distinción es crucial para entender el flujo de datos en operaciones compuestas:

Function<Integer, Integer> sumar5 = x -> x + 5;
Function<Integer, Integer> multiplicarPor2 = x -> x * 2;

// Diferentes órdenes de composición
Function<Integer, Integer> primeroSumarLuegoMultiplicar = sumar5.andThen(multiplicarPor2);
Function<Integer, Integer> primeroMultiplicarLuegoSumar = sumar5.compose(multiplicarPor2);

Integer resultado1 = primeroSumarLuegoMultiplicar.apply(10);  // (10 + 5) * 2 = 30
Integer resultado2 = primeroMultiplicarLuegoSumar.apply(10);  // (10 * 2) + 5 = 25

Composición múltiple de funciones

Una ventaja significativa de estos métodos es la capacidad de encadenar múltiples transformaciones:

Function<String, String> recortar = s -> s.trim();
Function<String, String> mayusculas = s -> s.toUpperCase();
Function<String, Integer> longitud = s -> s.length();

// Encadenamiento de múltiples funciones
Function<String, Integer> procesarTexto = recortar
    .andThen(mayusculas)
    .andThen(longitud);

Integer resultado = procesarTexto.apply("  hola mundo  "); // 10

Este enfoque permite construir pipelines de procesamiento claros y mantenibles.

Aplicaciones prácticas

La composición de funciones es especialmente útil en escenarios como:

  • Procesamiento de datos en etapas:
class Empleado {
    private String nombre;
    private double salarioBase;
    
    // Constructor y getters
    public Empleado(String nombre, double salarioBase) {
        this.nombre = nombre;
        this.salarioBase = salarioBase;
    }
    
    public String getNombre() { return nombre; }
    public double getSalarioBase() { return salarioBase; }
}

Function<Empleado, Double> calcularSalarioBase = Empleado::getSalarioBase;
Function<Double, Double> aplicarBonificacion = salario -> salario * 1.1;
Function<Double, Double> aplicarImpuestos = salario -> salario * 0.85;
Function<Double, String> formatearSalario = salario -> String.format("%.2f€", salario);

// Pipeline completo de procesamiento
Function<Empleado, String> calcularSalarioFinal = calcularSalarioBase
    .andThen(aplicarBonificacion)
    .andThen(aplicarImpuestos)
    .andThen(formatearSalario);

String salarioFormateado = calcularSalarioFinal.apply(new Empleado("Ana", 30000));
// "28050.00€" (30000 → bonificación → impuestos → formato)
  • Validación en cadena:
Function<String, String> validarNoVacio = s -> {
    if (s == null || s.trim().isEmpty()) 
        throw new IllegalArgumentException("El texto no puede estar vacío");
    return s;
};

Function<String, String> validarLongitudMinima = s -> {
    if (s.length() < 8) 
        throw new IllegalArgumentException("El texto debe tener al menos 8 caracteres");
    return s;
};

Function<String, String> validarContenido = s -> {
    if (!s.matches(".*[0-9].*")) 
        throw new IllegalArgumentException("El texto debe contener al menos un número");
    return s;
};

// Composición de validaciones
Function<String, String> validarPassword = validarNoVacio
    .andThen(validarLongitudMinima)
    .andThen(validarContenido);

try {
    String passwordValidada = validarPassword.apply("secreta123");
    System.out.println("Password válida: " + passwordValidada);
} catch (IllegalArgumentException e) {
    System.out.println("Error de validación: " + e.getMessage());
}

Manejo de excepciones en composición

Al componer funciones, es importante considerar cómo se propagan las excepciones:

Function<String, Integer> parseEntero = s -> {
    try {
        return Integer.parseInt(s);
    } catch (NumberFormatException e) {
        return 0; // Valor por defecto
    }
};

Function<Integer, Double> calcularRaizCuadrada = n -> {
    if (n < 0) throw new IllegalArgumentException("No se puede calcular la raíz de un número negativo");
    return Math.sqrt(n);
};

// Composición con manejo de excepciones
Function<String, Double> parseYCalcularRaiz = parseEntero.andThen(calcularRaizCuadrada);

Double resultado1 = parseYCalcularRaiz.apply("16");  // 4.0
Double resultado2 = parseYCalcularRaiz.apply("abc"); // 0.0 (parseEntero devuelve 0 para entradas inválidas)

Composición con identity()

El método estático identity() de la interfaz Function es útil en composiciones como elemento neutro:

Function<String, String> identidad = Function.identity();
Function<String, String> mayusculas = String::toUpperCase;

// La composición con identity() no altera el comportamiento
Function<String, String> soloMayusculas = identidad.andThen(mayusculas);
Function<String, String> tambiénMayusculas = mayusculas.compose(identidad);

String resultado1 = soloMayusculas.apply("java");      // "JAVA"
String resultado2 = tambiénMayusculas.apply("java");   // "JAVA"

Implementación de composición personalizada

Podemos crear nuestras propias utilidades de composición para casos específicos:

public static <A, B, C> Function<A, C> componer(
        Function<A, B> primera,
        Function<B, C> segunda) {
    return primera.andThen(segunda);
}

// Uso de nuestra utilidad
Function<Integer, Double> alCuadrado = n -> Math.pow(n, 2);
Function<Double, String> formatear = n -> String.format("%.1f", n);

Function<Integer, String> operacionCompleta = componer(alCuadrado, formatear);
String resultado = operacionCompleta.apply(5); // "25.0"

La composición de funciones es una herramienta poderosa que permite construir transformaciones complejas a partir de operaciones simples, siguiendo el principio de responsabilidad única. Los métodos andThen() y compose() facilitan este enfoque, permitiendo crear código más modular, legible y mantenible.

BiFunction y otras variantes

La interfaz Function que hemos estudiado hasta ahora es extremadamente útil, pero Java proporciona varias variantes especializadas para escenarios específicos. Estas interfaces amplían las capacidades de programación funcional, permitiendo trabajar con diferentes números de parámetros o con tipos específicos.

BiFunction: funciones con dos parámetros

La interfaz BiFunction<T, U, R> representa una función que acepta dos argumentos y produce un resultado:

@FunctionalInterface
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
    
    default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t, U u) -> after.apply(apply(t, u));
    }
}

Esta interfaz es ideal para operaciones que requieren dos entradas:

BiFunction<Integer, Integer, Integer> sumar = (a, b) -> a + b;
Integer resultado = sumar.apply(5, 3); // 8

BiFunction<String, Integer, String> repetir = (texto, veces) -> {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < veces; i++) {
        sb.append(texto);
    }
    return sb.toString();
};
String textoRepetido = repetir.apply("Java", 3); // "JavaJavaJava"

A diferencia de Function, BiFunction solo proporciona el método andThen() para composición, pero no compose(). Esto se debe a que no es posible componer de manera directa una función de un parámetro con una de dos parámetros:

BiFunction<String, Integer, String> formatear = (nombre, edad) -> 
    nombre + " tiene " + edad + " años";
Function<String, String> mayusculas = String::toUpperCase;

// Composición con andThen
BiFunction<String, Integer, String> formatearYMayusculas = 
    formatear.andThen(mayusculas);

String resultado = formatearYMayusculas.apply("Ana", 28); // "ANA TIENE 28 AÑOS"

UnaryOperator: cuando entrada y salida son del mismo tipo

UnaryOperator<T> es una especialización de Function<T, T> donde el tipo de entrada y salida son idénticos:

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

Esta interfaz simplifica el código cuando trabajamos con transformaciones que no cambian el tipo:

// En lugar de esto:
Function<String, String> mayusculas = s -> s.toUpperCase();

// Podemos usar esto:
UnaryOperator<String> mayusculas = String::toUpperCase;

String texto = mayusculas.apply("hola mundo"); // "HOLA MUNDO"

UnaryOperator hereda los métodos compose() y andThen() de Function, permitiendo la composición de operaciones:

UnaryOperator<Integer> duplicar = n -> n * 2;
UnaryOperator<Integer> incrementar = n -> n + 1;

// Composición de operadores
UnaryOperator<Integer> duplicarYLuegoIncrementar = duplicar.andThen(incrementar);
UnaryOperator<Integer> incrementarYLuegoDuplicar = duplicar.compose(incrementar);

Integer resultado1 = duplicarYLuegoIncrementar.apply(5); // (5 * 2) + 1 = 11
Integer resultado2 = incrementarYLuegoDuplicar.apply(5); // (5 + 1) * 2 = 12

BinaryOperator: operaciones binarias con tipos idénticos

BinaryOperator<T> extiende BiFunction<T, T, T> y representa operaciones donde ambos parámetros y el resultado son del mismo tipo:

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T, T, T> {
    static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
    }
    
    static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
}

Esta interfaz es perfecta para operaciones como suma, multiplicación o concatenación:

BinaryOperator<Integer> multiplicar = (a, b) -> a * b;
Integer producto = multiplicar.apply(4, 5); // 20

BinaryOperator<List<String>> unirListas = (lista1, lista2) -> {
    List<String> resultado = new ArrayList<>(lista1);
    resultado.addAll(lista2);
    return resultado;
};

List<String> lista1 = Arrays.asList("a", "b");
List<String> lista2 = Arrays.asList("c", "d");
List<String> unidas = unirListas.apply(lista1, lista2); // [a, b, c, d]

BinaryOperator proporciona dos métodos estáticos útiles: minBy() y maxBy(), que crean operadores que seleccionan el mínimo o máximo de dos valores según un comparador:

// Encontrar el número menor
BinaryOperator<Integer> menor = BinaryOperator.minBy(Integer::compare);
Integer minimo = menor.apply(25, 13); // 13

// Encontrar la cadena más larga
Comparator<String> porLongitud = Comparator.comparing(String::length);
BinaryOperator<String> masCadenaLarga = BinaryOperator.maxBy(porLongitud);
String masLarga = masCadenaLarga.apply("Java", "Python"); // "Python"

IntFunction, LongFunction y DoubleFunction

Para mejorar el rendimiento cuando trabajamos con tipos primitivos, Java proporciona variantes especializadas que evitan el boxing/unboxing:

@FunctionalInterface
public interface IntFunction<R> {
    R apply(int value);
}

@FunctionalInterface
public interface LongFunction<R> {
    R apply(long value);
}

@FunctionalInterface
public interface DoubleFunction<R> {
    R apply(double value);
}

Estas interfaces aceptan un tipo primitivo como entrada y devuelven un objeto:

IntFunction<String> formatearEntero = n -> String.format("Número: %d", n);
String texto = formatearEntero.apply(42); // "Número: 42"

DoubleFunction<BigDecimal> convertirABigDecimal = BigDecimal::valueOf;
BigDecimal decimal = convertirABigDecimal.apply(123.456); // BigDecimal de 123.456

ToIntFunction, ToLongFunction y ToDoubleFunction

De manera similar, existen interfaces para funciones que devuelven tipos primitivos:

@FunctionalInterface
public interface ToIntFunction<T> {
    int applyAsInt(T value);
}

@FunctionalInterface
public interface ToLongFunction<T> {
    long applyAsLong(T value);
}

@FunctionalInterface
public interface ToDoubleFunction<T> {
    double applyAsDouble(T value);
}

Estas interfaces son útiles para extraer valores numéricos de objetos:

ToIntFunction<String> contarCaracteres = String::length;
int longitud = contarCaracteres.applyAsInt("Programación"); // 12

class Producto {
    private double precio;
    
    public Producto(double precio) {
        this.precio = precio;
    }
    
    public double getPrecio() {
        return precio;
    }
}

ToDoubleFunction<Producto> extraerPrecio = Producto::getPrecio;
double precio = extraerPrecio.applyAsDouble(new Producto(29.99)); // 29.99

Funciones primitivas bidireccionales

Para operaciones que trabajan con tipos primitivos tanto en entrada como en salida, Java ofrece:

@FunctionalInterface
public interface IntToDoubleFunction {
    double applyAsDouble(int value);
}

@FunctionalInterface
public interface IntToLongFunction {
    long applyAsLong(int value);
}

// Y otras combinaciones similares

Ejemplo de uso:

IntToDoubleFunction calcularRaiz = n -> Math.sqrt(n);
double raiz = calcularRaiz.applyAsDouble(16); // 4.0

Funciones con tipos primitivos binarias

Para operaciones que aceptan dos parámetros primitivos:

@FunctionalInterface
public interface IntBinaryOperator {
    int applyAsInt(int left, int right);
}

@FunctionalInterface
public interface LongBinaryOperator {
    long applyAsLong(long left, long right);
}

@FunctionalInterface
public interface DoubleBinaryOperator {
    double applyAsDouble(double left, double right);
}

Estas interfaces son ideales para operaciones matemáticas eficientes:

IntBinaryOperator sumar = (a, b) -> a + b;
int suma = sumar.applyAsInt(10, 20); // 30

DoubleBinaryOperator potencia = Math::pow;
double resultado = potencia.applyAsDouble(2.0, 3.0); // 8.0

Aplicaciones prácticas de las variantes

Estas variantes especializadas son particularmente útiles en escenarios como:

  • Procesamiento de datos eficiente:
// Calcular estadísticas de una lista de números
List<Integer> numeros = Arrays.asList(1, 2, 3, 4, 5);

ToDoubleFunction<List<Integer>> calcularPromedio = lista -> {
    double suma = 0;
    for (int num : lista) {
        suma += num;
    }
    return lista.isEmpty() ? 0 : suma / lista.size();
};

double promedio = calcularPromedio.applyAsDouble(numeros); // 3.0
  • Transformaciones de objetos de dominio:
class Cliente {
    private String nombre;
    private String apellido;
    
    // Constructor y getters
    public Cliente(String nombre, String apellido) {
        this.nombre = nombre;
        this.apellido = apellido;
    }
    
    public String getNombre() { return nombre; }
    public String getApellido() { return apellido; }
}

BiFunction<String, String, Cliente> crearCliente = Cliente::new;
Cliente nuevoCliente = crearCliente.apply("Juan", "Pérez");

Function<Cliente, String> obtenerNombreCompleto = 
    cliente -> cliente.getNombre() + " " + cliente.getApellido();
String nombreCompleto = obtenerNombreCompleto.apply(nuevoCliente); // "Juan Pérez"
  • Operaciones de reducción:
BinaryOperator<Integer> maximo = (a, b) -> Math.max(a, b);

List<Integer> valores = Arrays.asList(5, 3, 9, 1, 7);
Integer valorMaximo = valores.stream()
    .reduce(Integer.MIN_VALUE, maximo); // 9
  • Transformaciones condicionales:
BiFunction<Integer, Boolean, String> formatearSegunCondicion = 
    (numero, formatoExtendido) -> formatoExtendido 
        ? String.format("El valor es: %d", numero) 
        : String.valueOf(numero);

String resultado1 = formatearSegunCondicion.apply(42, true);  // "El valor es: 42"
String resultado2 = formatearSegunCondicion.apply(42, false); // "42"

Creación de variantes personalizadas

Aunque Java proporciona muchas variantes predefinidas, podemos crear nuestras propias interfaces funcionales para casos específicos:

@FunctionalInterface
interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
    
    default <W> TriFunction<T, U, V, W> andThen(Function<? super R, ? extends W> after) {
        return (t, u, v) -> after.apply(apply(t, u, v));
    }
}

// Uso de nuestra interfaz personalizada
TriFunction<String, String, String, String> concatenar = 
    (a, b, c) -> a + b + c;
String resultado = concatenar.apply("Hola, ", "mundo ", "funcional!"); 
// "Hola, mundo funcional!"

// Composición con andThen
TriFunction<Integer, Integer, Integer, Integer> sumarTres = 
    (a, b, c) -> a + b + c;
Function<Integer, String> formatear = n -> "Suma: " + n;

TriFunction<Integer, Integer, Integer, String> sumarYFormatear = 
    sumarTres.andThen(formatear);
String resultadoFormateado = sumarYFormatear.apply(1, 2, 3); // "Suma: 6"

Las variantes de la interfaz Function proporcionan herramientas especializadas para diferentes escenarios, permitiendo escribir código más expresivo y eficiente. Al elegir la interfaz adecuada para cada situación, podemos mejorar tanto la legibilidad como el rendimiento de nuestras aplicaciones Java.

Aprende Java online

Otros ejercicios de programación de Java

Evalúa tus conocimientos de esta lección Interfaz funcional Function con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Streams: match

Test

Gestión de errores y excepciones

Código

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Clases abstractas

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

API java.nio 2

Puzzle

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Interfaces

Código

Enumeraciones Enums

Código

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

CRUD de productos en Java

Proyecto

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Herencia

Código

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Mapas

Código

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

Todas las lecciones de Java

Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Instalación De Java

Introducción Y Entorno

Configuración De Entorno Java

Introducción Y Entorno

Tipos De Datos

Sintaxis

Variables

Sintaxis

Operadores

Sintaxis

Estructuras De Control

Sintaxis

Funciones

Sintaxis

Recursión

Sintaxis

Arrays Y Matrices

Sintaxis

Excepciones

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Encapsulación

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Sobrecarga De Métodos

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

La Clase Scanner

Programación Orientada A Objetos

Métodos De La Clase String

Programación Orientada A Objetos

Excepciones

Programación Orientada A Objetos

Records

Programación Orientada A Objetos

Pattern Matching

Programación Orientada A Objetos

Inferencia De Tipos Con Var

Programación Orientada A Objetos

Enumeraciones Enums

Programación Orientada A Objetos

Generics

Programación Orientada A Objetos

Clases Sealed

Programación Orientada A Objetos

Listas

Framework Collections

Conjuntos

Framework Collections

Mapas

Framework Collections

Funciones Lambda

Programación Funcional

Interfaz Funcional Consumer

Programación Funcional

Interfaz Funcional Predicate

Programación Funcional

Interfaz Funcional Supplier

Programación Funcional

Interfaz Funcional Function

Programación Funcional

Métodos Referenciados

Programación Funcional

Creación De Streams

Programación Funcional

Operaciones Intermedias Con Streams: Map()

Programación Funcional

Operaciones Intermedias Con Streams: Filter()

Programación Funcional

Operaciones Intermedias Con Streams: Distinct()

Programación Funcional

Operaciones Finales Con Streams: Collect()

Programación Funcional

Operaciones Finales Con Streams: Min Max

Programación Funcional

Operaciones Intermedias Con Streams: Flatmap()

Programación Funcional

Operaciones Intermedias Con Streams: Sorted()

Programación Funcional

Operaciones Finales Con Streams: Reduce()

Programación Funcional

Operaciones Finales Con Streams: Foreach()

Programación Funcional

Operaciones Finales Con Streams: Count()

Programación Funcional

Operaciones Finales Con Streams: Match

Programación Funcional

Api Optional

Programación Funcional

Transformación

Programación Funcional

Reducción Y Acumulación

Programación Funcional

Mapeo

Programación Funcional

Streams Paralelos

Programación Funcional

Agrupación Y Partición

Programación Funcional

Filtrado Y Búsqueda

Programación Funcional

Api Java.nio 2

Entrada Y Salida Io

Fundamentos De Io

Entrada Y Salida Io

Leer Y Escribir Archivos

Entrada Y Salida Io

Httpclient Moderno

Entrada Y Salida Io

Clases De Nio2

Entrada Y Salida Io

Api Java.time

Api Java.time

Localtime

Api Java.time

Localdatetime

Api Java.time

Localdate

Api Java.time

Executorservice

Concurrencia

Virtual Threads (Project Loom)

Concurrencia

Future Y Completablefuture

Concurrencia

Spring Framework

Frameworks Para Java

Micronaut

Frameworks Para Java

Maven

Frameworks Para Java

Gradle

Frameworks Para Java

Lombok Para Java

Frameworks Para Java

Quarkus

Frameworks Para Java

Ecosistema Jakarta Ee De Java

Frameworks Para Java

Introducción A Junit 5

Testing

Accede GRATIS a Java y certifícate

Certificados de superación de Java

Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la definición y propósito de la interfaz funcional Function en Java.
  • Aprender a implementar y utilizar el método apply() para transformar datos.
  • Conocer cómo componer funciones usando andThen() y compose() para crear pipelines de procesamiento.
  • Identificar y aplicar variantes especializadas de Function como BiFunction, UnaryOperator y BinaryOperator.
  • Entender el uso de interfaces funcionales para tipos primitivos y cómo crear variantes personalizadas.