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.