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.
Los principios que sustentan la programación funcional proporcionan una guía para escribir código modular, expresivo y menos propenso a errores:
Una función lambda es una expresión anónima que representa una implementación concisa de una interfaz funcional.
La sintaxis básica de una función lambda en Java consta de tres partes:
→): Separa la declaración de parámetros del cuerpo de la función.{} ni la palabra clave return.
//Consumer<String> printer = (phrase) -> IO.println(phrase);
Consumer<String> printer = IO::println;
Supplier<Integer> randomValue = () -> (int) (Math.random() * 100);
Function<Integer, String> printNumber = (number) -> {
if (number % 2 == 0) {
return number + " es par";
} else {
return number + " es impar";
}
};
::).(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.
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"
}
}
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<T> | void accept(T t) | Recibe un valor y realiza una acción sin devolver nada (efecto secundario). | s → IO.println(s) |
Predicate<T> | boolean test(T t) | Evalúa una condición sobre el argumento y devuelve un booleano. | i → i > 0 |
Function<T, R> | R apply(T t) | Transforma un argumento de tipo T en un resultado de tipo R. | s → s.length() |
Supplier<T> | T get() | No recibe argumentos y genera/devuelve un valor de tipo T. | () → “Hello World” |
public class FunctionalStandard {
static void main() {
Consumer<String> printer = IO::println;
printer.accept("Hello, World!"); // Imprime Hello, World!
Predicate<Integer> 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<Integer, String> 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<String> greeting = () -> "¡Hola, Mundo!";
IO.println(greeting.get()); // Imprime "¡Hola, Mundo!"
}
}
Existen además variantes binarias como BiConsumer<T, U>, BiPredicate<T, U> o BiFunction<T, R U,> para operar sobre dos argumentos simultáneamente.
Un Stream representa una secuencia de elementos que permite la ejecución de operaciones de procesamiento de datos de forma declarativa, secuencial o paralela.
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.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.Stream solo puede recorrerse una vez. Tras ejecutar una operación terminal, el Stream queda cerrado y no se puede reutilizar.
//1. A partir de de colecciones
List<Integer> numberList = List.of(1, 2, 3, 4, 5);
Stream<Integer> 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<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Stream<Integer> 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<Integer> evenNumbersList = numbers.stream()
.filter(n -> n % 2 == 0)
.toList();
for (Integer evenNumber : evenNumbersList) {
IO.println("Even number: " + evenNumber);
}
}
}
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<T>): Retiene únicamente los elementos que cumplen la condición.map(Function<T, R>): Transforma cada elemento aplicando una función.sorted() / sorted(Comparator<T>): 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.
List<String>names = List.of("John", "Jane", "Bob", "Jack", "Doe");
List<String> 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<Integer> 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<String> sortedNames = names.stream()
.sorted()
.peek(name -> IO.println("Nombre ordenado: " + name)) // Muestra los nombres ordenados
.toList(); // Ordena los nombres alfabéticamente
List<Integer> numbers = List.of(1, 2, 2, 3, 4, 4, 5);
List<Integer> uniqueNumbers = numbers.stream()
.distinct()
.peek(number -> IO.println("Número único: " + number)) // Muestra los números únicos
.toList(); // Obtiene los números únicos
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<Integer> numbers = List.of(1, 19, 45, 19, 34, 99, 86);
// Crea una lista con los números pares
List<Integer> 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<Integer> 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<Integer> firstGreaterThan20 = numbers.stream()
.filter(n -> n > 20)
.findFirst();
// Obtiene cualquier elemento que es mayor que 20
Optional<Integer> 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);
Optional<T> 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:
NullPointerExceptions: Al encapsular un valor, Optional permite realizar operaciones de manera segura sin preocuparse por valores nulos no deseados.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.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.
Optional<Integer> opt = Optional.of(5); // Permite valores nulos (si es null, devuelve un Optional vacío) Optional<Integer> optNullable = Optional.ofNullable(nullableValue); // Instancia explícitamente vacía Optional<Integer> optEmpty = Optional.empty();
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<String> optionalString = Optional.of("Hola, mundo!");
Optional<String> 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
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.Optional es que un método que devuelve Optional<valor> 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.
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.
Dada una lista de enteros List<Integer>, calcula la suma de todos sus elementos utilizando la API Stream.
Dada una lista de enteros List<Integer>, devuelve una nueva lista que contenga únicamente los números pares.
Dada una lista de cadenas de texto List<String>, devuelve una nueva lista con todas las cadenas convertidas a mayúsculas.
Dada una lista de enteros List<Integer>, encuentra el número máximo utilizando Streams y Optional.
Dada una lista de enteros List<Integer> y un valor límite int limit, cuenta cuántos elementos de la lista son estrictamente mayores que dicho valor.
Dada una lista de cadenas List<String>, concatena todos sus elementos en una única cadena separada por comas utilizando Streams.
Dada una lista de enteros List<Integer>, devuelve una lista con el cuadrado de cada uno de sus números.
Dada una lista de enteros List<Integer> que contiene valores repetidos, devuelve una nueva lista sin duplicados.
Dada una lista de enteros List<Integer> 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.
Dada una lista de cadenas List<String> 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).