Dans Java 8, comment puis-je transformer une Map <K, V> en une autre Map <K, V> à l'aide d'un lambda?

140

Je viens de commencer à regarder Java 8 et pour essayer des lambdas, j'ai pensé essayer de réécrire une chose très simple que j'ai écrite récemment. Je dois transformer une carte de chaîne en colonne en une autre carte de chaîne en colonne où la colonne dans la nouvelle carte est une copie défensive de la colonne dans la première carte. La colonne a un constructeur de copie. Le plus proche que j'ai jusqu'à présent est:

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

mais je suis sûr qu'il doit y avoir une meilleure façon de le faire et je serais reconnaissant pour quelques conseils.

Annesadleir
la source

Réponses:

222

Vous pouvez utiliser un collecteur :

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
la source
6
Je pense que la chose importante (et légèrement contre-intuitive) à noter dans ce qui précède est que la transformation a lieu dans le collecteur, plutôt que dans une opération de flux 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}

Vous pouvez utiliser la forEachméthode pour faire ce que vous voulez.

Ce que vous faites là-bas, c'est:

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

Ce que nous pouvons simplifier en un lambda:

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

Et comme nous appelons simplement une méthode existante, nous pouvons utiliser une référence de méthode , ce qui nous donne:

map.forEach(map2::put);
Arrem
la source
8
J'aime la façon dont vous avez expliqué exactement ce qui se passe dans les coulisses ici, cela rend les choses beaucoup plus claires. Mais je pense que cela me donne juste une copie directe de ma carte originale, pas une nouvelle carte où les valeurs ont été transformées en copies défensives. Ai-je bien compris que la raison pour laquelle nous pouvons simplement utiliser (map2 :: put) est que les mêmes arguments vont dans le lambda, par exemple (s, integer) que dans la méthode put? Donc, pour faire une copie défensive, faudrait-il originalColumnMap.forEach((string, column) -> newColumnMap.put(string, new Column(column)));ou pourrais-je raccourcir cela?
annesadleir
Oui, vous l'êtes. Si vous ne faites que passer les mêmes arguments, vous pouvez utiliser une référence de méthode, mais dans ce cas, puisque vous avez le new Column(column)comme paramètre, vous devrez y aller.
Arrem
J'aime mieux cette réponse car elle fonctionne si les deux cartes vous sont déjà fournies, comme lors du renouvellement de session.
David Stanley
Pas sûr de comprendre l'intérêt d'une telle réponse. Il réécrit le constructeur de presque toutes les implémentations de Map à partir d'une Map existante - comme ceci .
Kineolyan le
13

La méthode sans réinsérer toutes les entrées dans la nouvelle carte devrait être la plus rapide , car elle HashMap.cloneeffectue également une reprise en interne.

Map<String, Column> newColumnMap = originalColumnMap.clone();
newColumnMap.replaceAll((s, c) -> new Column(c));
Leventov
la source
C'est une façon très lisible de le faire et assez concise. Il ressemble à HashMap.clone () Map<String,Column> newColumnMap = new HashMap<>(originalColumnMap);. Je ne sais pas si c'est différent sous les couvertures.
annesadleir
2
Cette approche a l'avantage de conserver le Mapcomportement de l' implémentation, c'est-à-dire que si Mapest an EnumMapou a SortedMap, le résultat le Mapsera également. Dans le cas d'un SortedMapavec un spécial, Comparatorcela peut faire une énorme différence sémantique. Eh bien, la même chose s'applique à un IdentityHashMap
Holger
8

Restez simple et utilisez 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
la source
dans ce cas: map.forEach (map2 :: put); is simple :)
ses
5

Si vous utilisez Guava (v11 minimum) dans votre projet, vous pouvez utiliser Maps :: transformValues .

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

Remarque: les valeurs de cette carte sont évaluées paresseusement. Si la transformation coûte cher, vous pouvez copier le résultat sur une nouvelle carte comme suggéré dans la documentation 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
la source
Remarque: selon la documentation, cela renvoie une vue évaluée paresseuse sur l'originalColumnMap, de sorte que la fonction Column :: new est réévaluée chaque fois que l'entrée est accédée (ce qui peut ne pas être souhaitable lorsque la fonction de mappage est coûteuse)
Erric
Correct. Si la transformation est coûteuse, vous êtes probablement d'accord avec la surcharge de la copie sur une nouvelle carte, comme suggéré dans la documentation. 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
C'est vrai, mais cela pourrait valoir la peine d'ajouter une note de bas de page dans la réponse pour ceux qui ont tendance à sauter la lecture de la documentation.
Erric
@Erric Cela a du sens, vient d'être ajouté.
Andrea Bergonzo
3

Voici un autre moyen qui vous donne accès à la clé et à la valeur en même temps, au cas où vous auriez à faire une sorte de transformation.

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
la source
1

Si cela ne vous dérange pas d'utiliser des bibliothèques tierces, ma bibliothèque cyclops- react a des extensions pour tous les types de collections JDK , y compris Map . Vous pouvez utiliser directement les méthodes map ou bimap pour transformer votre carte. Un MapX peut être construit à partir d'une carte existante, par exemple.

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

Si vous souhaitez également changer la clé, vous pouvez écrire

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

bimap peut être utilisé pour transformer les clés et les valeurs en même temps.

À mesure que MapX étend la carte, la carte générée peut également être définie comme

  Map<String, Column> y
John McClean
la source