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ícateDefinició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 entradaR
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 originalR
es el tipo de salida de la función originalV
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 compuestaT
es el tipo de entrada de la función originalR
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.
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
Gestión de errores y excepciones
CRUD en Java de modelo Customer sobre un ArrayList
Clases abstractas
Listas
Métodos de la clase String
Streams: reduce()
API java.nio 2
Polimorfismo
Pattern Matching
Streams: flatMap()
Llamada y sobrecarga de funciones
Métodos referenciados
Métodos de la clase String
Representación de Fecha
Operadores lógicos
Inferencia de tipos con var
Tipos de datos
Estructuras de iteración
Streams: forEach()
Objetos
Funciones lambda
Uso de Scanner
Tipos de variables
Streams: collect()
Operadores aritméticos
Arrays y matrices
Clases y objetos
Interfaz funcional Consumer
CRUD en Java de modelo Customer sobre un HashMap
Interfaces
Enumeraciones Enums
API Optional
Interfaz funcional Function
Encapsulación
Interfaces
Uso de API Optional
Representación de Hora
Herencia básica
Clases y objetos
Interfaz funcional Supplier
HashMap
Sobrecarga de métodos
Polimorfismo de tiempo de ejecución
OOP en Java
Sobrecarga de métodos
CRUD de productos en Java
Clases sealed
Creación de Streams
Records
Encapsulación
Streams: min max
Herencia
Métodos avanzados de la clase String
Funciones
Polimorfismo de tiempo de compilación
Reto sintaxis Java
Conjuntos
Estructuras de control
Recursión
Excepciones
Herencia avanzada
Estructuras de selección
Uso de interfaces
Operadores
Variables
HashSet
Objeto Scanner
Streams: filter()
Operaciones de Streams
Interfaz funcional Predicate
Streams: sorted()
Configuración de entorno
Uso de variables
Clases
Streams: distinct()
Streams: count()
ArrayList
Mapas
Datos de referencia
Interfaces funcionales
Métodos básicos de la clase String
Tipos de datos
Clases abstractas
Instalación
Funciones
Excepciones
Estructuras de control
Herencia de clases
La clase Scanner
Generics
Streams: map()
Funciones y encapsulamiento
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
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.