Wie transformiere ich in Java 8 eine Map <K, V> mit einem Lambda in eine andere Map <K, V>?

139

Ich habe gerade angefangen, mir Java 8 anzuschauen und Lambdas auszuprobieren. Ich dachte, ich würde versuchen, eine sehr einfache Sache, die ich kürzlich geschrieben habe, neu zu schreiben. Ich muss eine Map von String zu Column in eine andere Map von String zu Column umwandeln, wobei die Column in der neuen Map eine defensive Kopie der Column in der ersten Map ist. Die Spalte hat einen Kopierkonstruktor. Das nächste, was ich bisher habe, ist:

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

aber ich bin sicher, es muss einen schöneren Weg geben, und ich wäre dankbar für einige Ratschläge.

Annesadleir
quelle

Antworten:

221

Sie könnten einen Collector verwenden :

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
quelle
6
Ich denke, das Wichtige (und etwas kontraintuitive), was oben zu beachten ist, ist, dass die Transformation im Kollektor stattfindet und nicht in einer map () - Stream-Operation
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}

Mit der forEachMethode können Sie tun, was Sie wollen.

Was Sie dort tun, ist:

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

Was wir zu einem Lambda vereinfachen können:

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

Und weil wir nur eine vorhandene Methode aufrufen, können wir eine Methodenreferenz verwenden , die uns Folgendes gibt:

map.forEach(map2::put);
Arrem
quelle
8
Mir gefällt, wie Sie genau erklärt haben, was hier hinter den Kulissen vor sich geht, es macht es viel klarer. Aber ich denke, das gibt mir nur eine direkte Kopie meiner ursprünglichen Karte, keine neue Karte, bei der die Werte in defensive Kopien umgewandelt wurden. Verstehe ich Sie richtig, dass der Grund, den wir nur verwenden können (map2 :: put), darin besteht, dass in das Lambda dieselben Argumente eingehen, z. B. (s, integer) wie in die put-Methode? Um eine defensive Kopie zu erstellen, müsste es sein originalColumnMap.forEach((string, column) -> newColumnMap.put(string, new Column(column)));oder könnte ich das verkürzen?
Annesadleir
Ja, das bist Du. Wenn Sie nur dieselben Argumente übergeben, können Sie eine Methodenreferenz verwenden. In diesem Fall müssen Sie jedoch, da Sie die new Column(column)als Paremeter haben, damit fortfahren.
Arrem
Diese Antwort gefällt mir besser, da sie funktioniert, wenn Ihnen beide Karten bereits zur Verfügung gestellt wurden, z. B. bei der Sitzungserneuerung.
David Stanley
Ich bin mir nicht sicher, ob ich den Sinn einer solchen Antwort verstehen soll. Der Konstruktor der meisten Map-Implementierungen wird aus einer vorhandenen Map neu geschrieben - so .
Kineolyan
13

Der Weg ohne erneutes Einfügen aller Einträge in die neue Karte sollte der schnellste sein , der nicht möglich ist, da auch HashMap.cloneintern ein erneutes Aufwärmen durchgeführt wird.

Map<String, Column> newColumnMap = originalColumnMap.clone();
newColumnMap.replaceAll((s, c) -> new Column(c));
leventov
quelle
Das ist eine sehr lesenswerte und ziemlich prägnante Methode. Es sieht genauso aus wie HashMap.clone () Map<String,Column> newColumnMap = new HashMap<>(originalColumnMap);. Ich weiß nicht, ob es unter der Decke anders ist.
Annesadleir
2
Dieser Ansatz hat den Vorteil, dass das MapVerhalten der Implementierung beibehalten wird, dh wenn Mapes sich um ein EnumMapoder ein handelt SortedMap, ist dies auch das Ergebnis Map. Im Falle eines SortedMapmit einem speziellen kann Comparatores einen großen semantischen Unterschied machen. Na ja, das gilt auch für einen IdentityHashMap
Holger
8

Halten Sie es einfach und verwenden Sie 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
quelle
in diesem Fall: map.forEach (map2 :: put); ist einfach :)
ses
5

Wenn Sie in Ihrem Projekt Guava (mindestens Version 11) verwenden, können Sie Maps :: transformValues ​​verwenden .

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

Hinweis: Die Werte dieser Karte werden träge ausgewertet. Wenn die Umwandlung teuer ist, können Sie das Ergebnis auf eine neue Karte kopieren, wie in den Guava-Dokumenten vorgeschlagen.

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
quelle
Hinweis: Laut den Dokumenten wird eine verzögert ausgewertete Ansicht der originalColumnMap zurückgegeben, sodass die Funktion Column :: new bei jedem Zugriff auf den Eintrag neu bewertet wird (was möglicherweise nicht wünschenswert ist, wenn die Zuordnungsfunktion teuer ist)
Erric
Richtig. Wenn die Umwandlung teuer ist, ist der Aufwand für das Kopieren auf eine neue Karte, wie in den Dokumenten vorgeschlagen, wahrscheinlich in Ordnung. 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
Stimmt, aber es könnte sich lohnen, eine Fußnote in die Antwort aufzunehmen, wenn Sie dazu neigen, das Lesen der Dokumente zu überspringen.
Erric
@Erric Sinnvoll, gerade hinzugefügt.
Andrea Bergonzo
3

Hier ist eine andere Möglichkeit, mit der Sie gleichzeitig auf den Schlüssel und den Wert zugreifen können, falls Sie eine Transformation durchführen müssen.

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
quelle
1

Wenn es Ihnen nichts ausmacht, Bibliotheken von Drittanbietern zu verwenden, enthält meine Cyclops-React- Bibliothek Erweiterungen für alle JDK-Sammlungstypen , einschließlich Map . Sie können die Karten- oder Bimap-Methoden direkt verwenden, um Ihre Karte zu transformieren. Ein MapX kann aus einer vorhandenen Map erstellt werden, z.

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

Wenn Sie auch den Schlüssel ändern möchten, können Sie schreiben

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

Mit bimap können die Schlüssel und Werte gleichzeitig transformiert werden.

Wenn MapX Map erweitert, kann die generierte Map auch als definiert werden

  Map<String, Column> y
John McClean
quelle