Jak w Javie 8 przekształcić Map <K, V> w inną Map <K, V> używając lambda?

140

Właśnie zacząłem patrzeć na Javę 8 i wypróbować lambdy, pomyślałem, że spróbuję przepisać bardzo prostą rzecz, którą niedawno napisałem. Muszę przekształcić Map of String to Column w inną Map of String to Column, gdzie kolumna w nowej mapie jest obronną kopią kolumny z pierwszej mapy. Kolumna ma konstruktora kopiującego. Najbliższe, jakie do tej pory uzyskałem, to:

    Map<String, Column> newColumnMap= new HashMap<>();
    originalColumnMap.entrySet().stream().forEach(x -> newColumnMap.put(x.getKey(), new Column(x.getValue())));

ale jestem pewien, że musi być lepszy sposób na zrobienie tego i byłbym wdzięczny za radę.

annesadleir
źródło

Odpowiedzi:

222

Możesz użyć Kolektora :

import java.util.*;
import java.util.stream.Collectors;

public class Defensive {

  public static void main(String[] args) {
    Map<String, Column> original = new HashMap<>();
    original.put("foo", new Column());
    original.put("bar", new Column());

    Map<String, Column> copy = original.entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey,
                                  e -> new Column(e.getValue())));

    System.out.println(original);
    System.out.println(copy);
  }

  static class Column {
    public Column() {}
    public Column(Column c) {}
  }
}
McDowell
źródło
6
Myślę, że ważną (i nieco sprzeczną z intuicją) rzeczą, na którą należy zwrócić uwagę w powyższym, jest to, że transformacja zachodzi w kolektorze, a nie w operacji strumienia map ()
Brian Agnew
24
Map<String, Integer> map = new HashMap<>();
map.put("test1", 1);
map.put("test2", 2);

Map<String, Integer> map2 = new HashMap<>();
map.forEach(map2::put);

System.out.println("map: " + map);
System.out.println("map2: " + map2);
// Output:
// map:  {test2=2, test1=1}
// map2: {test2=2, test1=1}

Możesz użyć tej forEachmetody, aby zrobić, co chcesz.

To, co tam robisz, to:

map.forEach(new BiConsumer<String, Integer>() {
    @Override
    public void accept(String s, Integer integer) {
        map2.put(s, integer);     
    }
});

Które możemy uprościć do lambdy:

map.forEach((s, integer) ->  map2.put(s, integer));

A ponieważ po prostu wywołujemy istniejącą metodę, możemy użyć odwołania do metody , które daje nam:

map.forEach(map2::put);
Arrem
źródło
8
Podoba mi się, jak dokładnie wyjaśniłeś, co dzieje się za kulisami, dzięki temu jest to znacznie jaśniejsze. Ale myślę, że to daje mi prostą kopię mojej oryginalnej mapy, a nie nową mapę, na której wartości zostały zamienione na kopie obronne. Czy dobrze rozumiem, że powodem, dla którego możemy po prostu użyć (map2 :: put) jest to, że te same argumenty trafiają do lambda, np. (S, integer), co w metodzie put? Czy więc konieczne byłoby wykonanie kopii obronnej, originalColumnMap.forEach((string, column) -> newColumnMap.put(string, new Column(column)));czy mogę ją skrócić?
annesadleir
Tak, jesteś. Jeśli przekazujesz tylko te same argumenty, możesz użyć odwołania do metody, jednak w tym przypadku, ponieważ masz new Column(column)parometr, musisz to zrobić.
Arrem
Ta odpowiedź podoba mi się bardziej, ponieważ działa, jeśli obie mapy są już dostarczone, na przykład podczas odnawiania sesji.
David Stanley
Nie jestem pewien, aby zrozumieć sens takiej odpowiedzi. To przepisywanie konstruktora większości implementacji Map z istniejącej mapy - w ten sposób .
Kineolyan
13

Sposób bez ponownego wstawiania wszystkich wpisów do nowej mapy powinien być najszybszy , ponieważ HashMap.clonewewnętrznie wykonuje również ponowne haszowanie.

Map<String, Column> newColumnMap = originalColumnMap.clone();
newColumnMap.replaceAll((s, c) -> new Column(c));
Leventov
źródło
To bardzo czytelny sposób na zrobienie tego i dość zwięzły. Wygląda na to, że tak samo jak HashMap.clone () jest Map<String,Column> newColumnMap = new HashMap<>(originalColumnMap);. Nie wiem, czy pod kołdrą jest inaczej.
annesadleir
2
Takie podejście ma tę zaletę, że zachowuje zachowanie Mapimplementacji, tzn. Jeśli Mapjest a EnumMaplub a SortedMap, wynik Mapbędzie również. W przypadku SortedMapze specjalnością Comparatormoże to spowodować ogromną różnicę semantyczną. No cóż, to samo dotyczy IdentityHashMap
Holger
8

Zachowaj prostotę i używaj Java 8: -

 Map<String, AccountGroupMappingModel> mapAccountGroup=CustomerDAO.getAccountGroupMapping();
 Map<String, AccountGroupMappingModel> mapH2ToBydAccountGroups = 
              mapAccountGroup.entrySet().stream()
                         .collect(Collectors.toMap(e->e.getValue().getH2AccountGroup(),
                                                   e ->e.getValue())
                                  );
Anant Khurana
źródło
w tym przypadku: map.forEach (map2 :: put); jest proste :)
sesja
5

Jeśli używasz w swoim projekcie guawy (minimum v11), możesz użyć Maps :: transformValues .

Map<String, Column> newColumnMap = Maps.transformValues(
  originalColumnMap,
  Column::new // equivalent to: x -> new Column(x) 
)

Uwaga: wartości tej mapy są oceniane leniwie. Jeśli transformacja jest kosztowna, możesz skopiować wynik do nowej mapy, jak sugerowano w dokumentacji Guava.

To avoid lazy evaluation when the returned map doesn't need to be a view, copy the returned map into a new map of your choosing.
Andrea Bergonzo
źródło
Uwaga: zgodnie z dokumentacją zwraca to leniwy szacowany widok na originalColumnMap, więc funkcja Column :: new jest ponownie oceniana za każdym razem, gdy uzyskuje się dostęp do wpisu (co może nie być pożądane, gdy funkcja mapowania jest droga)
Erric
Poprawny. Jeśli transformacja jest kosztowna, prawdopodobnie wystarczy skopiować ją na nową mapę, jak sugerowano w dokumentacji. To avoid lazy evaluation when the returned map doesn't need to be a view, copy the returned map into a new map of your choosing.
Andrea Bergonzo
To prawda, ale może warto dodać jako przypis w odpowiedzi dla tych, którzy mają tendencję do pomijania czytania dokumentów.
Erric
@Erric Ma sens, właśnie dodane.
Andrea Bergonzo,
3

Oto inny sposób, który daje dostęp do klucza i wartości w tym samym czasie, na wypadek, gdybyś musiał dokonać jakiejś transformacji.

Map<String, Integer> pointsByName = new HashMap<>();
Map<String, Integer> maxPointsByName = new HashMap<>();

Map<String, Double> gradesByName = pointsByName.entrySet().stream()
        .map(entry -> new AbstractMap.SimpleImmutableEntry<>(
                entry.getKey(), ((double) entry.getValue() /
                        maxPointsByName.get(entry.getKey())) * 100d))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Lucas Ross
źródło
1

Jeśli nie masz nic przeciwko korzystaniu z bibliotek innych firm, moja biblioteka cyclops-react ma rozszerzenia dla wszystkich typów kolekcji JDK , w tym Map . Możesz bezpośrednio użyć metody map lub bimap, aby przekształcić swoją mapę. MapX można zbudować z istniejącej mapy, np.

  MapX<String, Column> y = MapX.fromMap(orgColumnMap)
                               .map(c->new Column(c.getValue());

Jeśli chcesz również zmienić klucz, możesz napisać

  MapX<String, Column> y = MapX.fromMap(orgColumnMap)
                               .bimap(this::newKey,c->new Column(c.getValue());

bimap może służyć do jednoczesnego przekształcania kluczy i wartości.

W miarę jak MapX rozszerza Map, wygenerowana mapa może być również zdefiniowana jako

  Map<String, Column> y
John McClean
źródło