Volver a Java Intermedio

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.