En este tutorial rápido, vamos a hablar sobre el método toMap() de la clase Collectors. Lo utilizaremos para recopilar Streams en una instancia de Map.

Para todos los ejemplos incluidos aquí, usaremos una lista de libros como punto de partida y la transformaremos en diferentes implementaciones de mapas.

Comenzaremos con el caso más simple, transformando una Lista en un Mapa.

Nuestra clase de libro se define como:

class Libro {
    private String nombre;
    private int lanzamiento;
    private String isbn;
    // ...
}

Y crearemos una lista de libros para validar su código:

List<Libro> lista = new ArrayList<>();
lista.add(new Libro("La comunidad del anillo", 1954, "0395489318"));
lista.add(new Book("Las dos torres", 1954, "0345339711"));
lista.add(new Book("El regreso del rey", 1955, "0618129111"));

Para este escenario usaremos la siguiente sobrecarga del método toMap():

Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)

Con toMap, podemos indicar estrategias para obtener la clave y el valor del mapa:

public Map<String, String> listaAMapa(List<Libro> libros) {
    return libros.stream().collect(Collectors.toMap(Libro::getIsbn, Libro::getNombre));
}

Y podemos validarlo fácilmente funciona con una prueba unitaria:

@Test
public void cuandoConvierteDeListaAMapa() {
    assertTrue(listaAMapa(listaDeLibros).size() == 3);
}

El ejemplo anterior funcionó bien, pero ¿qué pasaría si hubiera una clave duplicada?

Imaginemos que hemos marcado nuestro Mapa para el año de publicación de cada Libro:

public Map<Integer, Libro> listaAMapaConDuplicados(List<Libro> libros) {
    return libros.stream().collect(Collectors.toMap(Libro::getLanzamiento, Function.identity()));
}

Dada nuestra lista anterior de libros, veríamos una excepción IllegalStateException:

@Test(expected = IllegalStateException.class)
public void listaAMapaConDuplicados_sin_consolidar_arroja_runtime_exception() {
    listaAMapaConDuplicados(libros);
}

Para resolverlo, necesitamos usar un método diferente con un parámetro adicional, el mergeFunction:

Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
  Function<? super T, ? extends U> valueMapper,
  BinaryOperator<U> mergeFunction)

Introduzcamos una función de combinación que indica que, en el caso de una colisión, mantenemos la entrada existente:

public Map<Integer, Book> listaAMapaConDuplicados(List<Libro> libros) {
    return books.stream().collect(Collectors.toMap(Book::getLanzamiento, Function.identity(),
      (existing, replacement) -> existing));
}

O, en otras palabras, obtenemos un comportamiento “first-win”:

@Test
public void listaAMapaConDuplicadosConMergeFunction() {
    Map<Integer, Libro> librosPorAnio = listaAMapaConDuplicados(lista);
    assertEquals(2, librosPorAnio.size());
    assertEquals("0395489318", librosPorAnio.get(1954).getIsbn());
}

De forma predeterminada, un método toMap() devolverá un HashMap.

¿Pero podemos devolver diferentes implementaciones de mapas? La respuesta es sí:

Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
  Function<? super T, ? extends U> valueMapper,
  BinaryOperator<U> mergeFunction,
  Supplier<M> mapSupplier)

Donde mapSupplier es una función que devuelve un Mapa nuevo y vacío con los resultados.

Tomemos el mismo ejemplo que el anterior y agreguemos una función mapSupplier para devolver un ConcurrentHashMap:

public Map<Integer, Libro> listaAMapaConcurrente(List<Libro> libros) {
    return libros.stream().collect(Collectors.toMap(Libro::getLanzamiento, Function.identity(),
      (o1, o2) -> o1, ConcurrentHashMap::new));
}

Sigamos y probemos nuestro código:

@Test
public void creartHashMapConcurrente() {
    assertTrue(listaAMapaConcurrente(listaLibros) instanceof ConcurrentHashMap);
}

Por último, veamos cómo devolver un mapa ordenado. Para eso tendremos que ordenar una lista y utilizar un TreeMap como un parámetro mapSupplier:

public TreeMap<String, Libro> listaAMapaOrdenado(List<Libro> libros) {
    return libros.stream() 
      .sorted(Comparator.comparing(Libro::getNombre))
      .collect(Collectors.toMap(Libro::getNombre, Function.identity(), (o1, o2) -> o1, TreeMap::new));
}

El código anterior ordenará la lista según el nombre del libro y luego recopilará los resultados en un Mapa de árbol:

@Test
public void whenListaAMapaOrdenado() {
    assertTrue(listaAMapaOrdenado(lista).firstKey().equals("La comunidad del anillo"));
}

Categorized in:

Tagged in:

, ,