Introducción
La programación funcional enfatiza la inmutabilidad, funciones puras y código declarativo. Java 8+ introdujo muchas características de programación funcional que hacen el código más conciso y mantenible.
Funciones Puras
Funciones que siempre retornan la misma salida para la misma entrada y no tienen efectos secundarios:
// Función pura
public int sumar(int a, int b) {
return a + b;
}
// Función impura (tiene efectos secundarios)
private int contador = 0;
public int incrementoImpuro() {
return ++contador; // Modifica estado externo
}
Inmutabilidad
Usar objetos inmutables previene bugs y hace el código thread-safe:
// Clase inmutable
public final class Punto {
private final int x;
private final int y;
public Punto(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
// Retornar nueva instancia en lugar de modificar
public Punto mover(int dx, int dy) {
return new Punto(x + dx, y + dy);
}
}
Composición de Funciones
Combinar funciones simples para crear comportamientos complejos:
Function<Integer, Integer> multiplicarPorDos = x -> x * 2;
Function<Integer, Integer> sumarTres = x -> x + 3;
// Componer funciones
Function<Integer, Integer> multiplicarLuegoSumar =
multiplicarPorDos.andThen(sumarTres);
System.out.println(multiplicarLuegoSumar.apply(5)); // (5 * 2) + 3 = 13
// Composición inversa
Function<Integer, Integer> sumarLuegoMultiplicar =
multiplicarPorDos.compose(sumarTres);
System.out.println(sumarLuegoMultiplicar.apply(5)); // (5 + 3) * 2 = 16
Optional - Manejo de Ausencia
Optional<String> nombre = Optional.of("Alicia");
Optional<String> vacio = Optional.empty();
// Verificar si está presente
if (nombre.isPresent()) {
System.out.println(nombre.get());
}
// Mejor: usar métodos funcionales
nombre.ifPresent(System.out::println);
// Proporcionar valor por defecto
String resultado = vacio.orElse("Desconocido");
String resultado2 = vacio.orElseGet(() -> "Por Defecto Calculado");
// Transformar valores
Optional<Integer> longitud = nombre.map(String::length);
System.out.println(longitud.orElse(0)); // 6
// Encadenar
Optional<String> mayus = nombre
.filter(n -> n.length() > 3)
.map(String::toUpperCase);
Predicados
Condiciones reutilizables:
Predicate<Integer> esPar = n -> n % 2 == 0;
Predicate<Integer> esPositivo = n -> n > 0;
Predicate<Integer> esGrande = n -> n > 100;
// Combinar predicados
Predicate<Integer> esParYPositivo = esPar.and(esPositivo);
Predicate<Integer> esParOGrande = esPar.or(esGrande);
Predicate<Integer> esImpar = esPar.negate();
List<Integer> numeros = Arrays.asList(-5, 2, 8, 15, 102);
List<Integer> filtrados = numeros.stream()
.filter(esParYPositivo)
.collect(Collectors.toList());
// [2, 8, 102]
Suppliers y Consumers
// Supplier - produce valores
Supplier<Double> proveedorAleatorio = Math::random;
System.out.println(proveedorAleatorio.get());
Supplier<List<String>> proveedorLista = ArrayList::new;
List<String> lista = proveedorLista.get();
// Consumer - consume valores
Consumer<String> impresor = System.out::println;
Consumer<String> registrador = msg -> log.info(msg);
Consumer<String> ambos = impresor.andThen(registrador);
ambos.accept("Hola"); // Imprime y registra
Referencias de Métodos
Sintaxis más limpia para expresiones lambda:
// Lambda
lista.forEach(item -> System.out.println(item));
// Referencia de método
lista.forEach(System.out::println);
// Tipos de referencias de métodos
// 1. Método estático
Function<String, Integer> parseador = Integer::parseInt;
// 2. Método de instancia de objeto particular
String prefijo = "Hola: ";
Function<String, String> saludador = prefijo::concat;
// 3. Método de instancia de objeto arbitrario
Function<String, String> aMayus = String::toUpperCase;
// 4. Referencia a constructor
Supplier<List<String>> creadorLista = ArrayList::new;
Function<String, Integer> creadorInt = Integer::new;
Currificación
Convertir funciones multi-argumento en cadena de funciones de un solo argumento:
// Función tradicional
BiFunction<Integer, Integer, Integer> sumar = (a, b) -> a + b;
// Versión currificada
Function<Integer, Function<Integer, Integer>> sumarCurrificada =
a -> b -> a + b;
// Uso
Function<Integer, Integer> sumarCinco = sumarCurrificada.apply(5);
System.out.println(sumarCinco.apply(3)); // 8
System.out.println(sumarCinco.apply(10)); // 15
Ejemplos Prácticos
Configuración con Optional:
public class Config {
private final Map<String, String> propiedades = new HashMap<>();
public Optional<String> obtener(String clave) {
return Optional.ofNullable(propiedades.get(clave));
}
public int obtenerInt(String clave, int valorDefecto) {
return obtener(clave)
.map(Integer::parseInt)
.orElse(valorDefecto);
}
public <T> T obtenerOLanzar(String clave, Supplier<T> parseador) {
return obtener(clave)
.map(parseador)
.orElseThrow(() -> new ConfigException("Falta: " + clave));
}
}
Procesamiento en pipeline:
public class ProcesadorDatos {
private final List<Function<String, String>> pipeline = new ArrayList<>();
public ProcesadorDatos agregarPaso(Function<String, String> paso) {
pipeline.add(paso);
return this;
}
public String procesar(String entrada) {
return pipeline.stream()
.reduce(Function.identity(), Function::andThen)
.apply(entrada);
}
}
// Uso
ProcesadorDatos procesador = new ProcesadorDatos()
.agregarPaso(String::trim)
.agregarPaso(String::toLowerCase)
.agregarPaso(s -> s.replaceAll("\\s+", "_"));
String resultado = procesador.procesar(" Hola Mundo ");
// "hola_mundo"
Cadena de validación:
public class Validador<T> {
private final List<Predicate<T>> reglas = new ArrayList<>();
public Validador<T> agregarRegla(Predicate<T> regla) {
reglas.add(regla);
return this;
}
public boolean validar(T valor) {
return reglas.stream().allMatch(regla -> regla.test(valor));
}
}
// Uso
Validador<String> validadorUsuario = new Validador<String>()
.agregarRegla(s -> s.length() >= 3)
.agregarRegla(s -> s.length() <= 20)
.agregarRegla(s -> s.matches("[a-zA-Z0-9_]+"));
boolean esValido = validadorUsuario.validar("usuario_123");
Buenas Prácticas
- Prefiere estructuras de datos inmutables
- Escribe funciones puras cuando sea posible
- Usa Optional para evitar verificaciones de null
- Compone funciones pequeñas en más grandes
- Usa referencias de métodos para claridad
- Evita efectos secundarios en expresiones lambda
- Usa interfaces funcionales de java.util.function
Conclusión
Los conceptos de programación funcional hacen el código más predecible, testeable y mantenible. Al adoptar inmutabilidad y funciones puras, puedes escribir software más confiable.
En el próximo capítulo, exploraremos hilos y concurrencia.