CodeGym /Cursos /JAVA 25 SELF /Uso de lambdas en colecciones y streams

Uso de lambdas en colecciones y streams

JAVA 25 SELF
Nivel 48 , Lección 1
Disponible

1. Expresiones lambda y colecciones

Recordemos cómo era el procesamiento de colecciones antes de la aparición de las expresiones lambda. Supongamos que tenemos una lista de cadenas y queremos mostrarlas por pantalla:

List<String> list = Arrays.asList("kot", "pyos", "yozh");

for (String s : list) {
    System.out.println(s);
}

Todo es sencillo, pero si queremos, por ejemplo, eliminar de la lista todas las cadenas vacías, hay que escribir un bucle con una condición y, a veces, usar un iterador (de lo contrario habrá una ConcurrentModificationException). O, por ejemplo, ordenar por la longitud de la cadena:

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

Incluso con una tarea tan simple, ya son 5 líneas de código y un montón de llaves «ruidosas». ¿Cómo optimizarlo? La respuesta ya la sabes: expresiones lambda.

Uso de expresiones lambda en métodos de colecciones

Desde Java 8, las interfaces de colecciones recibieron nuevos métodos que aceptan interfaces funcionales, lo que significa que podemos pasarles expresiones lambda. Aquí tienes las más populares:

  • forEach(Consumer<T> action)
  • removeIf(Predicate<T> filter)
  • sort(Comparator<T> c)
  • replaceAll(UnaryOperator<T> operator)

Ejemplo: forEach

Mostrar todos los elementos de la lista en pantalla (manera antigua):

for (String s : list) {
    System.out.println(s);
}

Ahora, con una lambda:

list.forEach(s -> System.out.println(s));

O aún más corto, si quieres jugar a ser «gurú de Java»:

list.forEach(System.out::println); // method reference, lo veremos más adelante

Ejemplo: removeIf

Eliminar todas las cadenas vacías de la lista:

List<String> animals = new ArrayList<>(Arrays.asList("kot", "", "pyos", "yozh", ""));
animals.removeIf(s -> s.isEmpty());
System.out.println(animals); // [kot, pyos, yozh]

Ejemplo: sort

Ordenar la lista por la longitud de la cadena:

List<String> animals = Arrays.asList("kot", "pyos", "yozh", "slon");

animals.sort((a, b) -> a.length() - b.length());
System.out.println(animals); // [kot, pyos, yozh, slon]

Ejemplo: replaceAll

Convertir todas las cadenas a mayúsculas:

List<String> animals = new ArrayList<>(Arrays.asList("kot", "pyos", "yozh"));
animals.replaceAll(s -> s.toUpperCase());
System.out.println(animals); // [KOT, PYOS, YOZH]

2. Stream API y expresiones lambda

Con la llegada de Java 8 apareció el Stream API, una potente herramienta para procesar colecciones con un estilo funcional. Los streams permiten filtrar, transformar, ordenar y recopilar colecciones mediante cadenas de métodos. ¡Y todos estos métodos aceptan expresiones lambda!

Importante: El análisis completo de Stream API vendrá más adelante; por ahora, solo ejemplos básicos para entender el papel de las lambdas.

Ejemplo: filtrado

Dejar solo las cadenas de más de 3 caracteres:

List<String> animals = Arrays.asList("kot", "slon", "yozh", "krokodil");
animals.stream()
       .filter(s -> s.length() > 3)
       .forEach(System.out::println); // slon, krokodil

Ejemplo: transformación (map)

Poner todas las cadenas en mayúsculas:

List<String> animals = Arrays.asList("kot", "slon", "yozh");
List<String> upper = animals.stream()
                            .map(s -> s.toUpperCase())
                            .collect(Collectors.toList());
System.out.println(upper); // [KOT, SLON, YOZH]

Ejemplo: ordenación

Obtener una lista ordenada por longitud (sin modificar la original):

List<String> animals = Arrays.asList("kot", "slon", "yozh", "krokodil");
List<String> sorted = animals.stream()
                             .sorted((a, b) -> a.length() - b.length())
                             .collect(Collectors.toList());
System.out.println(sorted); // [kot, slon, yozh, krokodil]

3. Comparación con clases anónimas

Comparemos cómo se vería el mismo código con una clase anónima y con una lambda.

Ordenación por longitud de cadena

Clase anónima:

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

Expresión lambda:

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

Conclusión:
Una expresión lambda ahorra un montón de líneas y hace el código más legible. ¡Menos llaves, menos ruido y más esencia!

Eliminación de cadenas vacías

Clase anónima:

animals.removeIf(new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.isEmpty();
    }
});

Expresión lambda:

animals.removeIf(s -> s.isEmpty());

4. Práctica: tareas cortas con expresiones lambda

Probemos en la práctica aplicar expresiones lambda en un mini-programa para trabajar con una lista de usuarios.

Ejemplo 1: Filtrar usuarios por edad

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

List<User> users = Arrays.asList(
    new User("Alisa", 17),
    new User("Bob", 25),
    new User("Charli", 15)
);

users.stream()
     .filter(u -> u.age >= 18)
     .forEach(System.out::println); // Bob (25)

Ejemplo 2: Ordenar usuarios por nombre

List<User> users = Arrays.asList(
    new User("Alisa", 17),
    new User("Bob", 25),
    new User("Charli", 15)
);

users.sort((u1, u2) -> u1.name.compareTo(u2.name));
System.out.println(users);
// [Alisa (17), Bob (25), Charli (15)]

Ejemplo 3: Transformar la lista de usuarios en una lista de nombres

List<String> names = users.stream()
                          .map(u -> u.name)
                          .collect(Collectors.toList());
System.out.println(names); // [Alisa, Bob, Charli]

Ejemplo 4: Eliminar a todos los menores de edad

List<User> users = new ArrayList<>(Arrays.asList(
    new User("Alisa", 17),
    new User("Bob", 25),
    new User("Charli", 15)
));

users.removeIf(u -> u.age < 18);
System.out.println(users); // [Bob (25)]

5. Matices útiles

Particularidades: ámbito y variables «efectivamente final»

Una expresión lambda puede usar variables del método externo, pero solo si son final o «efectivamente final» (es decir, no se modifican después de su inicialización).

int minAge = 18;
users.stream()
     .filter(u -> u.age >= minAge)
     .forEach(System.out::println);

Si intentas modificar minAge después de usarla en la lambda, el compilador dará un error.

Tabla: métodos principales de colecciones y streams con expresiones lambda

Método de colección/stream Qué hace Tipo de expresión lambda Ejemplo
forEach(Consumer<T>)
Para cada elemento
x -> ...
list.forEach(s -> ...)
removeIf(Predicate<T>)
Elimina elementos por una condición
x -> ...
list.removeIf(s -> ...)
sort(Comparator<T>)
Ordena los elementos
(a, b) -> ...
list.sort((a, b) -> ...)
replaceAll(UnaryOperator<T>)
Sustituye cada elemento
x -> ...
list.replaceAll(s -> ...)
filter(Predicate<T>)
Filtra el stream
x -> ...
stream.filter(s -> ...)
map(Function<T, R>)
Transforma los elementos
x -> ...
stream.map(s -> ...)
forEach(Consumer<T>)
Iteración sobre el stream
x -> ...
stream.forEach(s -> ...)
sorted(Comparator<T>)
Ordenación en el stream
(a, b) -> ...
stream.sorted((a, b) -> ...)

7. Errores típicos

Error n.º 1: La lambda es demasiado larga. Si dentro de la expresión lambda ya tienes 5 líneas de código, condiciones, bucles y try-catch, lo más probable es que debas extraer ese código a un método aparte. Las lambdas son buenas para lógica corta.

Error n.º 2: Uso de variables mutables. Si intentas dentro de la lambda modificar una variable del método externo (por ejemplo, un contador), el compilador no te dejará. La variable debe ser final o efectivamente final.

Error n.º 3: Olvidar que los métodos de colecciones/streams no siempre modifican la colección original. Por ejemplo, stream().filter(...) no modifica la lista original, sino que devuelve un nuevo stream. Si quieres obtener una colección, usa collect(Collectors.toList()).

Error n.º 4: La expresión lambda no coincide con el tipo. Si un método acepta, por ejemplo, un Comparator<T>, y tú intentas pasar una lambda con un solo parámetro (y no con dos), habrá un error de compilación.

Error n.º 5: Pierdes legibilidad con lambdas anidadas. Si tienes una cadena de map, filter, forEach y dentro de cada lambda hay otra lambda, el código se vuelve ilegible. En esos casos, es mejor dividir las expresiones en pasos separados o extraer partes a métodos.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION