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ę.

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) {}
  }
}

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);

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));

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())
                                  );

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.

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));

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.