1. Introducción
En este tutorial, discutiremos algunos ejemplos de cómo utilizar los Streams de Java para trabajar con Maps. Es importante mencionar que algunos de estos ejercicios podrían resolverse utilizando una estructura de datos Map bidireccional. Sin embargo, aquí nos enfocaremos en un enfoque funcional.
Primero, explicaremos la idea básica que utilizaremos para trabajar con Maps y Streams. Luego, presentaremos un par de problemas relacionados con Maps y sus soluciones concretas utilizando Streams.
2. Idea Básica
La principal consideración que debemos tener en cuenta es que los Streams son secuencias de elementos que se pueden obtener fácilmente de una Colección. Por otro lado, los Maps tienen una estructura diferente, con una asignación de claves a valores y sin secuencia inherente. Sin embargo, esto no significa que no podamos convertir una estructura de Map en diferentes secuencias que nos permitan trabajar de manera natural con la API de Streams.
Veamos formas de obtener diferentes Colecciones a partir de un Map, que luego podemos convertir en un Stream:
Map<String, Integer> someMap = new HashMap<>();
Set<Map.Entry<String, Integer>> entries = someMap.entrySet();
Set<String> keySet = someMap.keySet();
Collection<Integer> values = someMap.values();
Stream<Map.Entry<String, Integer>> entriesStream = entries.stream();
Stream<Integer> valuesStream = values.stream();
Stream<String> keysStream = keySet.stream();
3. Obtener las Claves de un Map Usando Streams
3.1. Datos de Entrada
Supongamos que tenemos un Map:
Map<String, String> books = new HashMap<>();
books.put("978-0201633610", "Design patterns : elements of reusable object-oriented software");
books.put("978-1617291999", "Java 8 in Action: Lambdas, Streams, and functional-style programming");
books.put("978-0134685991", "Effective Java");
Estamos interesados en encontrar el ISBN del libro titulado “Effective Java”.
3.2. Recuperando una Coincidencia
Dado que el título del libro podría no existir en nuestro Map, queremos poder indicar que no hay un ISBN asociado para él. Podemos usar un Optional para expresar eso:
Optional<String> optionalIsbn = books.entrySet().stream()
.filter(e -> "Effective Java".equals(e.getValue()))
.map(Map.Entry::getKey)
.findFirst();
assertEquals("978-0134685991", optionalIsbn.get());
Analicemos el código: primero obtenemos el entrySet del Map. Solo queremos considerar las entradas que tengan “Effective Java” como título. La primera operación intermedia será un filter.
No estamos interesados en la entrada completa del Map, sino en la clave de cada entrada. Así que el siguiente paso es un mapeo que generará un nuevo Stream conteniendo solo las claves que coinciden con el título que estábamos buscando. Como solo queremos un resultado, aplicamos el método findFirst(), que proporcionará el valor inicial en el Stream como un objeto Optional.
Miremos un caso en el que un título no existe:
Optional<String> optionalIsbn = books.entrySet().stream()
.filter(e -> "Non Existent Title".equals(e.getValue()))
.map(Map.Entry::getKey).findFirst();
assertEquals(false, optionalIsbn.isPresent());
3.3. Recuperando Múltiples Resultados
Ahora cambiemos el problema para ver cómo podríamos devolver múltiples resultados en lugar de uno. Para tener múltiples resultados, adicionemos el siguiente libro a nuestro Map:
books.put("978-0321356680", "Effective Java: Second Edition");
Entonces, si buscamos todos los libros que comienzan con “Effective Java”, obtendremos más de un resultado de vuelta:
List<String> isbnCodes = books.entrySet().stream()
.filter(e -> e.getValue().startsWith("Effective Java"))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
assertTrue(isbnCodes.contains("978-0321356680"));
assertTrue(isbnCodes.contains("978-0134685991"));
Lo que hemos hecho en este caso es cambiar la condición del filtro para verificar si el valor en el Map comienza con “Effective Java” en lugar de comparar la igualdad exacta de cadenas. Esta vez, colectamos los resultados en lugar de simplemente seleccionar el primero, y los metimos en una List.
4. Obtener los Valores de un Map Usando Streams
Ahora enfoquemos un problema diferente con maps. En lugar de obtener ISBN basados en los títulos, intentaremos conseguir títulos basados en los ISBN.
Utilicemos el Map original. Queremos encontrar títulos con un ISBN que comience con “978-0”.
List<String> titles = books.entrySet().stream()
.filter(e -> e.getKey().startsWith("978-0"))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
assertEquals(2, titles.size());
assertTrue(titles.contains("Design patterns : elements of reusable object-oriented software"));
assertTrue(titles.contains("Effective Java"));
Esta solución es similar a las soluciones de nuestro conjunto anterior de problemas; hacemos un stream del entry set y luego filtramos, mapeamos y colectamos. Si quisiéramos devolver solo la primera coincidencia, podríamos llamar al método findFirst() en lugar de recolectar todos los resultados en una List.
5. Conclusión
En este artículo, hemos demostrado cómo procesar un Map de manera funcional. En particular, hemos visto que una vez que cambiamos a utilizar las colecciones asociadas a los Maps, el procesamiento con Streams se vuelve mucho más fácil e intuitivo.
Consejos Prácticos
- Utiliza el Optional: Siempre que no estés seguro de si un valor existe, utiliza Optional para evitar NullPointerExceptions.
- Fluidez en tu código: Aprovecha la naturaleza encadenada de los Streams para que tu código sea más limpio y mantenible.
- Mejora el rendimiento: Considera el uso de operaciones paralelas (parallelStream()) cuando trabajes con grandes conjuntos de datos para mejorar el rendimiento.
- Conoce las API de Streams: Familiarízate con las operaciones disponibles en Streams como map, filter, collect, y reduce para aprovechar al máximo esta poderosa característica de Java.
Siguiendo estos consejos, podrás manipular y gestionar mejor tus datos utilizando Streams en Java, haciendo tu trabajo más eficiente y efectivo. ¡Feliz programación!