W3docs

Programación Funcional en Java

Introducción a los conceptos de programación funcional en Java: funciones de primer orden, inmutabilidad, funciones puras y composición.

La parte anterior — Collections Framework — trataba sobre contenedores: estructuras de datos que almacenan elementos y las operaciones (add, remove, iterate, sort, binarySearch) que operan sobre ellos. Esta parte aborda una capa diferente del mismo problema. En lugar de dónde viven los datos, nos centraremos en cómo expresar transformaciones sobre ellos — de forma clara, composicional, sin bucles redundantes ni variables acumuladoras.

Ese cambio tiene un nombre. La programación funcional es el estilo donde el cómputo se expresa como la aplicación de funciones a valores, donde las funciones en sí son valores de primer orden y donde los datos suelen tratarse como inmutables. Java no fue diseñado como un lenguaje funcional — las clases, la mutación y los bucles explícitos son su núcleo —, pero desde Java 8 todo programa Java moderno toma prestado ampliamente de la caja de herramientas funcional. Ya escribiste algo de eso en la parte de colecciones: list.sort(Comparator.comparing(Person::name)), map.getOrDefault(k, 0), List.copyOf(source). Lo que hace la Parte 12 es nombrar el estilo explícitamente y proporcionarte el resto de las herramientas — lambdas, interfaces funcionales, los tipos de java.util.function, referencias a métodos, Optional y la API Stream — para que los patrones que has estado imitando se conviertan en movimientos de primer orden.

Cuatro ideas que definen el estilo

Los lenguajes puramente funcionales (Haskell, Erlang, F#) llevan las cuatro al límite. Java las aplica con moderación. Las cuatro ideas:

  1. Las funciones son valores de primer orden. Puedes pasar una función como argumento, devolverla desde un método, almacenarla en un campo o construirla en tiempo de ejecución.
  2. Funciones puras. Una función pura depende únicamente de sus entradas y no modifica nada observable del mundo. Con la misma entrada devuelve la misma salida. Sin I/O, sin mutación de campos, sin ramificación dependiente del tiempo.
  3. Inmutabilidad por defecto. Las estructuras de datos no se modifican en el lugar; las transformaciones devuelven nuevos valores. Las referencias antiguas siguen siendo válidas.
  4. Composición. Las funciones más grandes se construyen combinando otras más pequeñas (f.andThen(g), pred.and(other), cmp.thenComparing(...)), no editándolas.

Ninguna de estas ideas es específica de Java. Son una forma de pensar que el lenguaje ahora admite a través de lambdas, referencias a métodos, la API Stream y las colecciones inmutables que acabas de conocer.

1. Funciones como valores

Antes de Java 8, no podías tener una variable cuyo valor fuera una función. Podías pasar un objeto cuya clase tuviera un único método — eso era lo que eran Runnable, Comparator y ActionListener —, pero la sintaxis era torpe:

list.sort(new Comparator<String>() {
  @Override
  public int compare(String a, String b) {
    return a.length() - b.length();
  }
});

El método único estaba envuelto en una declaración de clase anónima. Java 8 introdujo las expresiones lambda como sintaxis concisa para la misma idea:

list.sort((a, b) -> a.length() - b.length());

La lambda es el valor. Se compila como una instancia de la interfaz funcional que sea necesaria en el punto de llamada (aquí, Comparator<String>). El siguiente capítulo trata toda la sintaxis; por ahora lo importante es que las funciones en Java son ahora valores que puedes nombrar, almacenar y pasar.

2. Funciones puras

Una función pura es aquella cuyo valor de retorno depende únicamente de sus argumentos y cuya ejecución no tiene efectos secundarios observables. Math.sqrt(2) es pura. System.currentTimeMillis() no lo es — devuelve valores diferentes entre llamadas. list.add(x) no lo es — muta list.

Las funciones puras son valiosas porque:

  • Son fáciles de probar — sin configuración, sin mocks, simplemente assertEquals(expected, f(input)).
  • Son fáciles de paralelizar — dos llamadas puras pueden ejecutarse en hilos distintos sin sincronización.
  • Son fáciles de cachear — memoiza una vez, devuelve la misma respuesta siempre.
  • Se componen sin sorpresasf(g(x)) hace lo que sugiere leerlo.

La mayoría de los programas reales útiles no son 100 % puros (alguien tiene que escribir en una base de datos). La disciplina funcional consiste en hacer que el cómputo central sea puro y desplazar las partes impuras — I/O, tiempo, aleatoriedad, mutación — a los bordes. Los Streams fomentan esto: un pipeline de operaciones puras es correcto por construcción; uno impuro (stream().peek(x -> counter++)...) es un nido de errores.

3. Inmutabilidad

La viste en el capítulo anterior. List.of(...), Set.of(...), Map.of(...) y List.copyOf(...) producen colecciones que no se pueden modificar. Los Records (cubiertos más adelante) te dan clases de datos inmutables:

record Point(double x, double y) {
  Point translated(double dx, double dy) {
    return new Point(x + dx, y + dy);     // returns a NEW Point — does not mutate this
  }
}

Los valores inmutables son inherentemente seguros para hilos. Nunca presentan un estado intermedio "roto". Pueden compartirse libremente sin copia defensiva. Y hacen que las funciones puras sean prácticas — si los valores no pueden cambiar, una función que devuelve uno tiene garantizado ser determinista para esa parte del mundo.

4. Composición

La composición es "construir una función grande a partir de pequeñas." En Java, Function, Predicate y Comparator ofrecen operadores composicionales:

Function<String, String> trim  = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> clean = trim.andThen(upper);   // trim, then upper

Predicate<Integer> positive = n -> n > 0;
Predicate<Integer> even     = n -> n % 2 == 0;
Predicate<Integer> posEven  = positive.and(even);

Comparator<String> byLength    = Comparator.comparingInt(String::length);
Comparator<String> lengthThenA = byLength.thenComparing(Comparator.naturalOrder());

La API composicional es parte del valor. No escribes un helper que toma dos predicados y los une con && — escribes a.and(b). El estilo escala: una transformación de seis pasos puede leerse de arriba a abajo como una única expresión en lugar de seis bucles anidados con acumuladores intermedios.

Lo que Java conserva del lado imperativo

Java es multiparadigma. Las características funcionales añadidas en Java 8+ conviven con las características imperativas que han existido desde la versión 1.0. Algunas cosas siguen siendo imperativas a propósito:

  • Sentencias y flujo de control. if, for, while, try siguen siendo los bloques de construcción básicos; las lambdas no los reemplazan, reemplazan la verbosidad de las clases anónimas.
  • Variables locales mutables. Dentro del cuerpo de un método, int sum = 0; for (int x : xs) sum += x; sigue siendo idiomático.
  • Campos mutables donde tienen sentido. Los Builders, cachés y componentes de UI con estado siguen mutando.

El principio: usa el estilo funcional donde haga el código más claro, no como dogma. Un stream().mapToInt(Integer::intValue).sum() puro es más claro que un bucle manual. Un pipeline de composición lambda de seis pasos que nadie de tu equipo puede leer, no lo es.

Un ejemplo completo: imperativo vs funcional, lado a lado

El programa siguiente calcula la longitud media de las cadenas no vacías de una lista, de dos formas. La primera versión es imperativa — un acumulador mutable, un bucle explícito y una protección contra la división por cero. La segunda versión es funcional — un pipeline de stream con operaciones puras que se lee de arriba a abajo. El tercer fragmento construye valores compuestos de Predicate y Function a partir de otros más pequeños, mostrando la composición en acción.

java— editable, runs on the server

Qué aprender de la ejecución:

  • Ambas versiones calculan el mismo promedio. La imperativa declara dos contadores mutables y un cuerpo de bucle; la funcional encadena cinco operaciones con nombre que describen cada una el qué, no el cómo.
  • Predicate.and construyó una prueba compuesta (notNull.and(notBlank)) a partir de dos predicados más pequeños — sin necesidad de un nuevo método auxiliar. Eso es la composición en acción.
  • Function.andThen hizo lo mismo para un pipeline que produce valores: trim luego length, expresado como una única Function<String, Integer> compuesta.
  • Cada operación en el stream es pura: String::trim, la lambda s -> !s.isEmpty(), String::length — ninguna muta estado. Llamar a trimmedLen.apply(\" hi \") dos veces produjo la misma respuesta; esa es la garantía de determinismo que hace que las funciones puras sean seguras para memoizar y paralelizar.

Qué sigue

El modelo mental está en su lugar: las funciones son valores, las transformaciones puras se componen, la inmutabilidad te libera de una clase de errores. El siguiente capítulo, Java Lambda Expressions, introduce la sintaxis concreta(params) -> body — que hace que este estilo sea ergonómico en Java, además de las reglas sobre la captura de variables, el tipado objetivo y dónde puede aparecer una lambda.

Práctica

Práctica
Una función 'pura' en el sentido de la programación funcional es aquella que...
Una función 'pura' en el sentido de la programación funcional es aquella que...
Was this page helpful?