Java8: HashMap <X, Y> do HashMap <X, Z> przy użyciu Stream / Map-Reduce / Collector

209

Wiem, jak „przekształcić” prostą Javę Listz Y-> Z, tj .:

List<String> x;
List<Integer> y = x.stream()
        .map(s -> Integer.parseInt(s))
        .collect(Collectors.toList());

Teraz chciałbym zrobić w zasadzie to samo z Mapą, tj .:

INPUT:
{
  "key1" -> "41",    // "41" and "42"
  "key2" -> "42      // are Strings
}

OUTPUT:
{
  "key1" -> 41,      // 41 and 42
  "key2" -> 42       // are Integers
}

Rozwiązanie nie powinno ograniczać się do String-> Integer. Tak jak w Listpowyższym przykładzie, chciałbym wywołać dowolną metodę (lub konstruktor).

Benjamin M.
źródło

Odpowiedzi:

372
Map<String, String> x;
Map<String, Integer> y =
    x.entrySet().stream()
        .collect(Collectors.toMap(
            e -> e.getKey(),
            e -> Integer.parseInt(e.getValue())
        ));

To nie jest tak ładne, jak kod listy. Nie można budować nowych Map.Entrys w map()połączeniu, więc praca jest mieszana z collect()połączeniem.

John Kugelman
źródło
59
Można wymienić e -> e.getKey()z Map.Entry::getKey. Ale to kwestia gustu / stylu programowania.
Holger,
5
W rzeczywistości jest to kwestia wydajności, sugerujesz, że jesteś nieco lepszy od „stylu” lambda
Jon Burgin,
36

Oto kilka wariantów odpowiedzi Sotirios Delimanolis , która na początek była całkiem dobra (+1). Rozważ następujące:

static <X, Y, Z> Map<X, Z> transform(Map<? extends X, ? extends Y> input,
                                     Function<Y, Z> function) {
    return input.keySet().stream()
        .collect(Collectors.toMap(Function.identity(),
                                  key -> function.apply(input.get(key))));
}

Kilka punktów tutaj. Pierwszym z nich jest użycie symboli wieloznacznych w nazwach ogólnych; dzięki temu funkcja jest nieco bardziej elastyczna. Symbol wieloznaczny byłby konieczny, jeśli na przykład chcesz, aby mapa wyjściowa miała klucz, który jest nadklasą klucza mapy wejściowej:

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<CharSequence, Integer> output = transform(input, Integer::parseInt);

(Jest też przykład wartości mapy, ale jest ona naprawdę zmyślona i przyznaję, że ograniczenie znaku wieloznacznego dla Y pomaga tylko w skrajnych przypadkach.)

Drugim punktem jest to, że zamiast przepuszczać strumień nad mapą wejściową entrySet, uruchomiłem go nad keySet. To sprawia, że ​​kod jest nieco bardziej przejrzysty, kosztem konieczności pobierania wartości z mapy zamiast z wpisu mapy. Nawiasem mówiąc, początkowo miałem key -> keyjako pierwszy argument, toMap()a to z jakiegoś powodu nie powiodło się z błędem wnioskowania typu. Zmieniłem to na (X key) -> keydziałające, tak jak to zrobiłem Function.identity().

Jeszcze inna odmiana jest następująca:

static <X, Y, Z> Map<X, Z> transform1(Map<? extends X, ? extends Y> input,
                                      Function<Y, Z> function) {
    Map<X, Z> result = new HashMap<>();
    input.forEach((k, v) -> result.put(k, function.apply(v)));
    return result;
}

To używa Map.forEach()zamiast strumieni. Sądzę, że jest to jeszcze prostsze, ponieważ rezygnuje z kolektorów, których używanie w mapach jest nieco niezdarne. Powodem jest to, że Map.forEach()podaje klucz i wartość jako osobne parametry, podczas gdy strumień ma tylko jedną wartość - i musisz wybrać, czy użyć klucza, czy wpisu mapy jako tej wartości. Z drugiej strony brakuje w tym bogatej, gwałtownej dobroci innych podejść. :-)

Znaki Stuarta
źródło
11
Function.identity()może wyglądać fajnie, ale ponieważ pierwsze rozwiązanie wymaga wyszukiwania mapy / skrótu dla każdego wpisu, podczas gdy wszystkie inne rozwiązania nie, nie polecam tego.
Holger
13

Takie ogólne rozwiązanie

public static <X, Y, Z> Map<X, Z> transform(Map<X, Y> input,
        Function<Y, Z> function) {
    return input
            .entrySet()
            .stream()
            .collect(
                    Collectors.toMap((entry) -> entry.getKey(),
                            (entry) -> function.apply(entry.getValue())));
}

Przykład

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<String, Integer> output = transform(input,
            (val) -> Integer.parseInt(val));
Sotirios Delimanolis
źródło
Przyjemne podejście z użyciem generycznych Myślę, że można to nieco poprawić - zobacz moją odpowiedź.
Stuart Marks
13

Funkcja Guava Maps.transformValuesjest tym, czego szukasz, i ładnie działa z wyrażeniami lambda:

Maps.transformValues(originalMap, val -> ...)
Alex Krauss
źródło
Podoba mi się to podejście, ale uważaj, aby nie przekazać funkcji java.util.Funkcja. Ponieważ oczekuje com.google.common.base.Function, Eclipse podaje nieprzydatny błąd - mówi, że funkcja nie ma zastosowania do funkcji, co może być mylące: „Metoda transformValues ​​(mapa <K, V1>, funkcja <? Super V1 , V2>) w rodzaju Mapy nie ma zastosowania do argumentów (Mapa <Foo, Bar>, Funkcja <Bar, Baz>) ”
mskfisher
Jeśli musisz zdać java.util.Function, masz dwie opcje. 1. Unikaj tego problemu, używając lambda, aby umożliwić wnioskowanie typu Java. 2. Użyj odwołania do metody, takiego jak javaFunction :: Apply, aby utworzyć nową lambda, którą można wywnioskować na podstawie wnioskowania typu.
Joe
5

Moja biblioteka StreamEx, która ulepsza standardowy interfejs API strumienia, zapewnia EntryStreamklasę, która lepiej pasuje do przekształcania map:

Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();
Tagir Valeev
źródło
4

Alternatywą, która zawsze istnieje w celu uczenia się, jest zbudowanie własnego kolektora za pomocą Collector.of (), chociaż tutaj toMap () kolektor JDK jest zwięzły (+1 tutaj ).

Map<String,Integer> newMap = givenMap.
                entrySet().
                stream().collect(Collector.of
               ( ()-> new HashMap<String,Integer>(),
                       (mutableMap,entryItem)-> mutableMap.put(entryItem.getKey(),Integer.parseInt(entryItem.getValue())),
                       (map1,map2)->{ map1.putAll(map2); return map1;}
               ));
Ira
źródło
Zacząłem od tego niestandardowego kolektora jako podstawy i chciałem dodać, że przynajmniej w przypadku korzystania z parallelStream () zamiast stream () binaryOperator powinien zostać przepisany na coś bardziej zbliżonego, w map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1przeciwnym razie wartości zostaną utracone podczas zmniejszania.
user691154,
3

Jeśli nie masz nic przeciwko korzystaniu z bibliotek stron trzecich, moja biblioteka Cyclops- React ma rozszerzenia dla wszystkich typów kolekcji JDK , w tym Map . Możemy po prostu przekształcić mapę bezpośrednio za pomocą operatora „map” (domyślnie mapa działa na wartości na mapie).

   MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                                .map(Integer::parseInt);

bimap może być używany do transformacji kluczy i wartości jednocześnie

  MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                               .bimap(this::newKey,Integer::parseInt);
John McClean
źródło