In Java 8 come posso trasformare una mappa <K, V> in un'altra mappa <K, V> usando una lambda?


140

Ho appena iniziato a guardare Java 8 e per provare lambdas ho pensato di provare a riscrivere una cosa molto semplice che ho scritto di recente. Devo trasformare una Mappa di stringa in colonna in un'altra Mappa di stringa in colonna in cui la colonna nella nuova mappa è una copia difensiva della colonna nella prima mappa. La colonna ha un costruttore di copie. Il più vicino che ho finora è:

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

ma sono sicuro che ci deve essere un modo migliore per farlo e sarei grato per qualche consiglio.

Risposte:


222

Puoi usare un Collezionista :

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
Penso che la cosa importante (e leggermente contro-intuitiva) da notare in precedenza sia che la trasformazione ha luogo nel raccoglitore, piuttosto che in un'operazione di flusso di una mappa ()
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}

Puoi usare il forEachmetodo per fare quello che vuoi.

Quello che stai facendo lì è:

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

Che possiamo semplificare in una lambda:

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

E poiché stiamo solo chiamando un metodo esistente, possiamo usare un riferimento al metodo , che ci dà:

map.forEach(map2::put);

8
Mi piace come hai spiegato esattamente cosa sta succedendo dietro le quinte qui, lo rende molto più chiaro. Ma penso che mi dia solo una copia diretta della mia mappa originale, non una nuova mappa in cui i valori sono stati trasformati in copie difensive. Ti sto capendo correttamente che il motivo che possiamo usare (map2 :: put) è che gli stessi argomenti stanno entrando nel lambda, ad esempio (s, intero) come stanno andando nel metodo put? Quindi per fare una copia difensiva dovrebbe essere originalColumnMap.forEach((string, column) -> newColumnMap.put(string, new Column(column)));o potrei accorciarlo?
annesadleir,

Sì, sei tu. Se stai semplicemente passando gli stessi argomenti, puoi usare un riferimento al metodo, tuttavia in questo caso, dato che hai il new Column(column)paremetro, dovrai seguirlo.
Arrem,

Mi piace meglio questa risposta perché funziona se entrambe le mappe ti sono già state fornite, come nel rinnovo della sessione.
David Stanley,

Non sono sicuro di capire il punto di tale risposta. Sta riscrivendo il costruttore di quasi tutte le implementazioni della Mappa da una Mappa esistente - in questo modo .
Kineolyan,

13

Il modo in cui non è necessario reinserire tutte le voci nella nuova mappa dovrebbe essere il più veloce perché non HashMap.cloneesegue internamente anche la rehash.

Map<String, Column> newColumnMap = originalColumnMap.clone();
newColumnMap.replaceAll((s, c) -> new Column(c));

Questo è un modo molto leggibile per farlo e abbastanza conciso. Sembra che HashMap.clone () ci sia Map<String,Column> newColumnMap = new HashMap<>(originalColumnMap);. Non so se c'è qualcosa di diverso sotto le coperte.
annesadleir,

2
Questo approccio ha il vantaggio di mantenere il Mapcomportamento dell'implementazione, cioè se Mapè un EnumMapo un SortedMap, anche il risultato Mapsarà. Nel caso di uno SortedMapcon uno speciale Comparator, potrebbe fare un'enorme differenza semantica. Oh bene, lo stesso vale per un IdentityHashMap...
Holger

8

Keep it Simple e usa 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())
                                  );

in questo caso: map.forEach (map2 :: put); è semplice :)
ses

5

Se usi Guava (minimo v11) nel tuo progetto puoi usare Maps :: transformValues .

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

Nota: i valori di questa mappa vengono valutati pigramente. Se la trasformazione è costosa, puoi copiare il risultato in una nuova mappa come suggerito nei documenti di 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.

Nota: secondo i documenti, questo restituisce una vista lazy valutata su originalColumnMap, quindi la funzione Column :: new viene rivalutata ogni volta che si accede alla voce (il che potrebbe non essere desiderabile quando la funzione di mapping è costosa)
Erric

Corretta. Se la trasformazione è costosa probabilmente stai bene con il sovraccarico di copiarla su una nuova mappa come suggerito nei documenti. 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,

È vero, ma potrebbe valere la pena aggiungerlo come nota a piè di pagina nella risposta per coloro che tendono a saltare la lettura dei documenti.
Erric,

@Erric Ha senso, appena aggiunto.
Andrea Bergonzo,

3

Ecco un altro modo che ti dà accesso alla chiave e al valore allo stesso tempo, nel caso in cui tu debba fare un qualche tipo di trasformazione.

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

Se non ti dispiace usare librerie di terze parti, la mia libreria cyclops- reazioni ha estensioni per tutti i tipi di raccolte JDK , inclusa Map . Puoi utilizzare direttamente i metodi mappa o bimap per trasformare la tua mappa. Una MapX può essere costruita da una Mappa esistente ad es.

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

Se desideri anche cambiare la chiave, puoi scrivere

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

bimap può essere usato per trasformare chiavi e valori contemporaneamente.

Man mano che MapX estende la mappa, la mappa generata può anche essere definita come

  Map<String, Column> y
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.