Interfaz funcional Function

Avanzado
Java
Java
Actualizado: 08/05/2025

¡Desbloquea el curso completo!

IA
Ejercicios
Certificado
Entrar

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.

¿Te está gustando esta lección?

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

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.

Aprendizajes 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.

Completa Java y certifícate

Únete a nuestra plataforma y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración