====== 06 - Programación Funcional ======
La programación funcional es un paradigma que se centra en tratar a las funciones como elementos de primer orden. En este enfoque, los programas se construyen principalmente a través de la composición y aplicación de funciones. A diferencia del paradigma imperativo, donde se enfatiza la ejecución secuencial de instrucciones que modifican el estado de la aplicación, en la programación funcional se prima la evaluación de expresiones y la ausencia de efectos secundarios.
===== 1. Principios Fundamentales =====
Los principios que sustentan la programación funcional proporcionan una guía para escribir código modular, expresivo y menos propenso a errores:
* **Funciones como Ciudadanos de Primera Clase**: Las funciones se tratan como cualquier otro tipo de dato. Pueden asignarse a variables, pasarse como argumentos a otros métodos y devolverse como resultado.\\ \\
* **Inmutabilidad**: Una vez creado un objeto o estructura de datos, su estado no debe modificarse. En lugar de alterar el objeto existente, se generan nuevas instancias con los valores actualizados. Esto previene efectos colaterales y facilita la concurrencia.\\ \\
* **Pureza de las Funciones**: Una función pura produce siempre el mismo resultado para las mismas entradas y no genera efectos secundarios observables (como modificar variables globales o alterar archivos).\\ \\
* **Composición y Funciones de Orden Superior**: Las funciones de orden superior son aquellas que reciben otras funciones por parámetro o las devuelven como resultado, permitiendo construir abstracciones complejas combinando bloques simples.\\ \\
===== 2. Funciones Lambda e Interfaces Funcionales =====
Una función lambda es una expresión anónima que representa una implementación concisa de una interfaz funcional.
==== 2.1 Sintaxis de las Expresiones Lambda ====
La sintaxis básica de una función lambda en Java consta de tres partes:
* **Parámetros**: Entre paréntesis y separados por comas. Si el tipo puede ser inferido por el compilador, se puede omitir el tipo implícito. Si solo hay un parámetro, se pueden omitir los paréntesis.\\ \\
* **Operador Flecha (''->'')**: Separa la declaración de parámetros del cuerpo de la función.\\ \\
* **Cuerpo**: Si contiene una única expresión, no requiere llaves ''{}'' ni la palabra clave ''return''.\\ \\
//Consumer printer = (phrase) -> IO.println(phrase);
Consumer printer = IO::println;
Supplier randomValue = () -> (int) (Math.random() * 100);
Function printNumber = (number) -> {
if (number % 2 == 0) {
return number + " es par";
} else {
return number + " es impar";
}
};
Cuando el cuerpo de una expresión lambda se limita únicamente a invocar un método ya existente pasando directamente los mismos parámetros recibidos, Java permite simplificar la sintaxis utilizando el operador de doble dos puntos (''::'').\\ \\
En lugar de escribir la estructura completa de la función lambda ''(mensaje) -> IO.println(mensaje)'', podemos hacer referencia directa al método mediante ''IO::println'' (o ''IO::println'' a partir de Java 23+).
Esta sintaxis no ejecuta el método inmediatamente, sino que le indica al compilador que reutilice el método existente para implementar la interfaz funcional correspondiente, logrando un código más limpio y legible.
==== 2.2 Interfaces Funcionales ====
En Java, una interfaz funcional es aquella que contiene un único método abstracto. Constituye el tipo de dato subyacente para cualquier expresión lambda.
Para garantizar que una interfaz mantenga este contrato, se utiliza la anotación ''@FunctionalInterface'':
@FunctionalInterface
public interface IntegerOperation {
int operation(int a, int b);
}
public class IntegerOperationExample {
static void main() {
IntegerOperation integerOperation = Integer::sum;
int result = integerOperation.operation(5, 10);
IO.println("Resultado de la suma: " + result); // Imprime "Resultado de la suma: 15"
integerOperation = Integer::compare;
result = integerOperation.operation(5, 10);
IO.println("Resultado de la comparación: " + result); // Imprime "Resultado de la comparación: -1"
integerOperation = Integer::max;
result = integerOperation.operation(5, 10);
IO.println("Resultado de la maximización: " + result); // Imprime "Resultado de la maximización: 10"
}
}
==== 2.3 Interfaces Funcionales Estándar (java.util.function) ====
Java proporciona un conjunto de interfaces funcionales predefinidas en el paquete ''java.util.function'' para cubrir los casos de uso habituales:
^ Interfaz ^ Firma del Método ^ Propósito ^ Ejemplo ^
| ''Consumer'' | ''void accept(T t)'' | Recibe un valor y realiza una acción sin devolver nada (efecto secundario). | ''s -> IO.println(s)'' |
| ''Predicate'' | ''boolean test(T t)'' | Evalúa una condición sobre el argumento y devuelve un booleano. | ''i -> i > 0'' |
| ''Function'' | ''R apply(T t)'' | Transforma un argumento de tipo T en un resultado de tipo R. | ''s -> s.length()'' |
| ''Supplier'' | ''T get()'' | No recibe argumentos y genera/devuelve un valor de tipo T. | ''() -> "Hello World"'' |
public class FunctionalStandard {
static void main() {
Consumer printer = IO::println;
printer.accept("Hello, World!"); // Imprime Hello, World!
Predicate isPositive = (number) -> number > 0;
IO.println("¿Es 5 positivo? " + isPositive.test(5)); // Devuelve true
IO.println("¿Es -3 positivo? " + isPositive.test(-3)); // Devuelve false
Function printNumber = (number) -> {
if (number % 2 == 0) {
return number + " es par";
} else {
return number + " es impar";
}
};
IO.println(printNumber.apply(42)); // Devuelve "42 es par"
IO.println(printNumber.apply(43)); // Devuelve "43 es impar"
Supplier greeting = () -> "¡Hola, Mundo!";
IO.println(greeting.get()); // Imprime "¡Hola, Mundo!"
}
}
Existen además variantes binarias como ''BiConsumer'', ''BiPredicate'' o ''BiFunction'' para operar sobre dos argumentos simultáneamente.
===== 3. La API Stream (java.util.stream) =====
Un ''Stream'' representa una secuencia de elementos que permite la ejecución de operaciones de procesamiento de datos de forma declarativa, secuencial o paralela.
==== 3.1 Ciclo de Vida y Características ====
* **No almacena datos e Inmutabilidad**: Un ''Stream'' no es una estructura de datos ni modifica la fuente original (colección, ''array'', etc.). Aplica transformaciones de forma inmutable, generando una nueva secuencia de datos en cada paso.\\ \\
* **No es iterable**: La interfaz ''Stream'' no extiende de ''Iterable'', por lo que no se puede recorrer con un bucle ''for-each'' tradicional (''for (T item : stream)''). El procesamiento se realiza exclusivamente a través de sus propios métodos.\\ \\
* **Encadenamiento de Operaciones**: Permite encadenar múltiples operaciones intermedias de forma continua, formando una canalización (pipeline) de procesamiento.\\ \\
* **Evaluación Perezosa**: Las operaciones intermedias no ejecutan ningún cálculo en el momento de su definición. Solo se procesan los datos cuando se invoca obligatoriamente una operación terminal.\\ \\
* **Consumo Único**: Un ''Stream'' solo puede recorrerse una vez. Tras ejecutar una operación terminal, el ''Stream'' queda cerrado y no se puede reutilizar.\\ \\
==== 3.2 Creación de Streams ====
//1. A partir de de colecciones
List numberList = List.of(1, 2, 3, 4, 5);
Stream numberStream = numberList.stream();
// 2. A partir de un Array
String[] nameArray = {"Alice", "Bob", "Charlie"};
Stream streamFromArray = Arrays.stream(nameArray);
// 3. A partir de valores directos
Stream streamFromValues = Stream.of(1, 2, 3, 4, 5);
Estos son solo algunos ejemplos de cómo crear ''Streams'' en Java a partir de diferentes fuentes de datos. La versatilidad de los ''Streams'' permite procesar una amplia variedad de datos de manera funcional y eficiente.
Además, la clase ''Stream'' proporciona métodos como ''toList()'' y ''toArray()'' que facilitan la conversión de un ''Stream'' en colecciones tradicionales de Java. Estas operaciones de recopilación son útiles para interactuar con APIs que esperan tipos de colección estándar, proporcionando una forma fluida y eficiente de manipular y transformar datos en Java.
public class StreamExample {
static void main() {
List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Stream evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0);
/* Error: La clase Stream no es iterable
for (Integer evenNumber : evenNumbers) {
IO.println("Even number: " + evenNumber);
}*/
//
List evenNumbersList = numbers.stream()
.filter(n -> n % 2 == 0)
.toList();
for (Integer evenNumber : evenNumbersList) {
IO.println("Even number: " + evenNumber);
}
}
}
==== 3.3 Operaciones Intermedias y Terminales ====
=== Operaciones Intermedias ===
Transforman el ''Stream'' en otro ''Stream'' diferente y se pueden encadenar indefinidamente. Al ser perezosas, no procesan la información de inmediato. Algunas de las más comunes:
* ''filter(Predicate)'': Retiene únicamente los elementos que cumplen la condición.\\ \\
* ''map(Function)'': Transforma cada elemento aplicando una función.\\ \\
* ''sorted() / sorted(Comparator)'': Ordena los elementos según su orden natural o un comparador.\\ \\
* ''distinct()'': Elimina duplicados basándose en ''equals()''.\\ \\
* ''peek(Consumer)'': Realiza una acción sobre cada elemento a medida que se procesa (muy útil para depurar o hacer logging) sin modificar la secuencia del ''Stream''.\\ \\
Listnames = List.of("John", "Jane", "Bob", "Jack", "Doe");
List filteredNames = names.stream()
.filter(name -> name.startsWith("J"))
.peek(name -> IO.println("Nombre filtrado: " + name)) // Muestra los nombres filtrados
.toList(); // Filtra los nombres que empiezan por J
List lenghts = names.stream()
.map(String::length)
.peek(length -> IO.println("Longitud del nombre: " + length)) // Muestra la longitud de cada nombre
.toList(); // Mapea los nombres a su longitud
List sortedNames = names.stream()
.sorted()
.peek(name -> IO.println("Nombre ordenado: " + name)) // Muestra los nombres ordenados
.toList(); // Ordena los nombres alfabéticamente
List numbers = List.of(1, 2, 2, 3, 4, 4, 5);
List uniqueNumbers = numbers.stream()
.distinct()
.peek(number -> IO.println("Número único: " + number)) // Muestra los números únicos
.toList(); // Obtiene los números únicos
=== Operaciones Terminales ===
Inician el procesamiento de los datos y producen un resultado final o un efecto secundario. Algunas de las más comunes:
* ''toList()'' / ''collect(Collector)'': Recoge los elementos del ''Stream'' en una colección:\\ \\
* ''toList()'' (desde Java 16): Forma directa y moderna de obtener una lista inmutable.\\ \\
* ''collect(Collectors.toSet())'' / ''Collectors.toList()'': Permite recolectar en colecciones específicas (''Set'', ''Map'' o listas mutables tradicionales).\\ \\
* ''collect(Collector)'': Transforma los elementos del ''Stream'' en una colección personalizada (como ''Set'' o ''Map'') u otra estructura mediante la clase ''Collectors''.\\ \\
* ''forEach(Consumer)'': Ejecuta una acción para cada elemento de la secuencia.\\ \\
* ''reduce(...)'': Combina todos los elementos en un único valor mediante una operación de acumulación.\\ \\
* ''count()'': Devuelve el número total de elementos procesados como un valor de tipo ''long''.\\ \\
* ''max(Comparator) / min(Comparator)'': Devuelve un ''Optional'' con el elemento máximo o mínimo según el comparador proporcionado.\\ \\
* ''findFirst() / findAny()'': Devuelve un ''Optional'' con el primer elemento o con cualquier elemento que cumpla la condición.\\ \\
* ''anyMatch() / allMatch() / noneMatch(Predicate)'': Evalúan una condición y devuelven un valor booleano con el resultado.\\\ \\
List numbers = List.of(1, 19, 45, 19, 34, 99, 86);
// Crea una lista con los números pares
List evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.toList();
// Imprimir cada número incrementado en 5
numbers.forEach(n -> IO.println(n + 5));
// Sumar todos los números usando reduce
int sum = numbers.stream()
.reduce(0, Integer::sum);
// Filtrar números pares y contarlos
long countEven = numbers.stream()
.filter(n -> n % 2 == 0)
.count();
// Obtener el número máximo o lanza una excepción si la lista está vacía
int max = numbers.stream()
.max(Integer::compare)
.orElseThrow(() -> new RuntimeException("Lista vacía"));
List emptyList = List.of();
int max2 = emptyList.stream()
.max(Integer::compare)
.orElseThrow(() -> new RuntimeException("Lista vacía"));
// Obtiene el primer elemento que es mayor que 20
Optional firstGreaterThan20 = numbers.stream()
.filter(n -> n > 20)
.findFirst();
// Obtiene cualquier elemento que es mayor que 20
Optional anyGreaterThan20 = numbers.stream()
.filter(n -> n > 20)
.findAny();
// Comprueba si AL MENOS existe un elemento mayor que 20
boolean hasGreaterThan20 = numbers.stream()
.anyMatch(n -> n > 20);
// Comprueba si TODOS los elementos son mayor que 20
boolean allGreaterThan20 = numbers.stream()
.allMatch(n -> n > 20);
// Comprueba si NINGÚN elemento es mayor que 20
boolean noneGreaterThan20 = numbers.stream()
.noneMatch(n -> n > 20);
===== 4. La Clase Optional =====
''Optional'' es un contenedor que puede o no albergar un valor no nulo. Su objetivo principal es actuar como tipo de retorno en métodos para representar la ausencia de un resultado sin lanzar excepciones ''NullPointerException''.
Esta clase fue introducida para abordar el problema de las referencias nulas (''null'') que pueden llevar a ''NullPointerExceptions'' en tiempo de ejecución. Los ''Optional'' ofrecen una forma más segura y explícita de representar valores que pueden estar ausentes.
Su principal utilidad radica en:
* **Evitar ''NullPointerExceptions''**: Al encapsular un valor, ''Optional'' permite realizar operaciones de manera segura sin preocuparse por valores nulos no deseados.\\ \\
* **Claridad y Documentación del Código**: Al usar ''Optional'', se hace explícito en la firma del método que un valor puede estar presente o ausente, mejorando la claridad y la documentación del código.\\ \\
* **Mejor Práctica de Diseño**: Fomenta el diseño de métodos que devuelven o manipulan valores opcionales, promoviendo un código más robusto y fácil de mantener.\\ \\
* **API más Expresiva**: Proporciona métodos útiles para trabajar con valores opcionales, como ''orElse'', ''orElseGet'', ''orElseThrow'', ''ifPresent'', entre otros, que facilitan manejar casos de valores nulos de manera más elegante.
Los ''Optional'' son una herramienta esencial en Java para mejorar la robustez y la claridad del código al manejar la posibilidad de valores nulos de manera más segura y eficiente.
==== 4.1 Creación de Instancias ====
Optional opt = Optional.of(5);
// Permite valores nulos (si es null, devuelve un Optional vacío)
Optional optNullable = Optional.ofNullable(nullableValue);
// Instancia explícitamente vacía
Optional optEmpty = Optional.empty();
==== 4.2 Métodos Principales ====
* ''isPresent'': Verifica si el Optional contiene un valor.\\ \\
* ''orElse(T other)'': Devuelve el valor encapsulado si está presente; de lo contrario, devuelve el valor proporcionado como argumento.\\ \\
* ''orElseGet(Supplier extends T> supplier)'': Devuelve el valor encapsulado si está presente; de lo contrario, devuelve el resultado obtenido del ''Supplier'' proporcionado.\\ \\
* ''orElseThrow(Supplier extends X> exceptionSupplier)'': Devuelve el valor encapsulado si está presente; de lo contrario, lanza una excepción proporcionada por el ''Supplier''.\\ \\
* ''get()'': Devuelve el valor encapsulado si está presente; de lo contrario, lanza una excepción ''NoSuchElementException''.\\ \\
* ''ifPresent(Consumer super T> consumer)'': Ejecuta la acción proporcionada si el valor está presente.
Optional optionalString = Optional.of("Hola, mundo!");
Optional optionalStringEmpty = Optional.empty();
if(optionalString.isPresent()) {
IO.println(optionalString.get()); // Imprime "Hola, mundo!"
}
if (optionalStringEmpty.isPresent()) {
IO.println(optionalStringEmpty.get());
} else {
IO.println("El valor no está presente"); // Imprime "El valor no está presente"
}
optionalString.orElse("Valor por defecto"); // Devuelve "Hola, mundo!"
optionalStringEmpty.orElse("Valor por defecto"); // Devuelve "Valor por defecto"
optionalString.orElseGet(() -> "Valor por defecto"); // Devuelve "Hola, mundo!"
optionalStringEmpty.orElseGet(() -> "Valor por defecto"); // Devuelve "Valor por defecto"
optionalString.orElseThrow(() -> new RuntimeException("Valor no presente")); // Devuelve "Hola, mundo!"
optionalStringEmpty.orElseThrow(() -> new RuntimeException("Valor no presente")); // Lanza una excepción
optionalString.ifPresent(IO::println); // Imprime "Hola, mundo!"
optionalStringEmpty.ifPresent(IO::println); // No imprime nada
Es importante entender que ''Optional.empty()'' en Java **no es equivalente a ''null''**. Mientras ''null'' indica la ausencia total de valor y puede causar ''NullPointerExceptions'' si no se maneja correctamente, ''Optional.empty()'' representa explícitamente la ausencia de un valor dentro de un contenedor ''Optional'', sin la posibilidad de generar ''NullPointerExceptions''.\\ \\
Una ventaja clave de usar ''Optional'' es que un método que devuelve ''Optional'' siempre garantiza devolver un objeto ''Optional'', ya sea que contenga un valor o esté vacío. Esto promueve un diseño más seguro y claro en el flujo de datos de la aplicación, ya que obliga a los desarrolladores a manejar explícitamente la posible ausencia de valores sin recurrir a valores ''null'' no controlados.
Es una buena práctica utilizar ''Optional'' **únicamente como tipo de retorno para métodos** que pueden o no devolver un valor, pero no debe usarse como tipo de parámetro de entrada para métodos. Entre otras razones, la clase ''Optional'' fue diseñado principalmente para mejorar la seguridad y la claridad al manejar valores de retorno potencialmente nulos. Utilizarlo como parámetro de entrada no se alinea con este propósito y puede llevar a prácticas confusas o propensas a errores.
===== Ejercicios =====
=== Ejercicio 1 ===
Dada una lista de enteros List, calcula la suma de todos sus elementos utilizando la API Stream.
=== Ejercicio 2 ===
Dada una lista de enteros List, devuelve una nueva lista que contenga únicamente los números pares.
=== Ejercicio 3 ===
Dada una lista de cadenas de texto List, devuelve una nueva lista con todas las cadenas convertidas a mayúsculas.
=== Ejercicio 4 ===
Dada una lista de enteros List, encuentra el número máximo utilizando Streams y Optional.
=== Ejercicio 5 ===
Dada una lista de enteros List y un valor límite int limit, cuenta cuántos elementos de la lista son estrictamente mayores que dicho valor.
=== Ejercicio 6 ===
Dada una lista de cadenas List, concatena todos sus elementos en una única cadena separada por comas utilizando Streams.
=== Ejercicio 7 ===
Dada una lista de enteros List, devuelve una lista con el cuadrado de cada uno de sus números.
=== Ejercicio 8 ===
Dada una lista de enteros List que contiene valores repetidos, devuelve una nueva lista sin duplicados.
=== Ejercicio 9 ===
Dada una lista de enteros List y un valor de referencia int threshold, devuelve el primer número de la lista que sea mayor que dicho valor envuelto en un Optional.
=== Ejercicio 10 ===
Dada una lista de cadenas List y una letra de inicio String prefix, cuenta cuántas cadenas de la lista empiezan por dicha letra (sin importar mayúsculas o minúsculas).