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.

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

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

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

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

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.

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

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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.