2015-06-19 13 views
7

Ich habe zwei Karten zu kombinieren und glätten:Java 8 lambdas/Transformationen Mit zwei Karten

  • Map<A, Collection<B>> mapAB
  • Map<B, Collection<C>> mapBC

Ich möchte sie in eine Map<A, Collection<C>> mapAC verwandeln, und ich bin Ich frage mich, ob es einen reibungslosen Weg gibt, dies mit Lambdas und Transformationen zu tun. In meinem speziellen Fall sind die Sammlungen alle Sets, aber ich möchte das Problem für Sammlungen im Allgemeinen lösen.

Ein Gedanke, den ich hatte, war, zuerst die zwei Karten zu einem Map<A, Map<B, Collection<C>>> zu kombinieren und es dann zu glätten, aber ich bin für jede Annäherung offen.

Daten Hinweise: B sollten nur in der Werterfassung mit einem A, assoziiert auftreten und das gleiche gilt für mapBC (a gegeben C nur von einer B abgebildet wird). Infolgedessen sollte es nur ein Pfad von einer gegebenen A zu einem C gegeben werden, obwohl es A -> B Mappings werden, für die es keine B -> C Mappings und dort B -> C Mappings, für die sein kann, keine entsprechenden A -> B Mappings dort sind. Diese Waisen erscheinen einfach nicht in der resultierenden mapAC.

Aus Gründen des Vergleichs, hier ist ein Beispiel für eine reinen Imperativ Annäherung an das gleiche Problem:

Map<A, Collection<C>> mapAC = new HashMap<>(); 

for (Entry<A, Collection<B>> entry : mapAB.entrySet()) { 
    Collection<C> cs = new HashSet<>(); 

    for (B b : entry.getValue()) { 
     Collection<C> origCs = mapBC.get(b); 
     if (origCs != null) { 
      cs.addAll(origCs); 
     } 
    } 

    if (!cs.isEmpty()) { 
     mapAC.put(entry.getKey(), cs); 
    } 
} 
+0

Möchten Sie eine Verknüpfung von zwei Eins-zu-viele-Beziehungen ohne die mittlere Spalte? –

+0

@MikeSamuel Ja, das ist definitiv eine Möglichkeit, es zu betrachten. –

+1

Können Sie ein Beispiel für Daten hinzufügen? Zum Beispiel, wenn wir 'Map >' und 'Map >' haben, ist es für verschiedene Leute möglich, denselben Job zu haben, oder für wenige Jobs dieselben Tools zu verwenden? Ist also etwas wie "p1 -> {j1, j2}, p2 -> {j2, j3}" möglich? Auch 'job1 -> {tool1, tool2} job2 -> {tool2, tool3} job3 -> {tool4}'? Welche Ergebnisse erwarten Sie? Möchten Sie auch 'Collection ' 'Set' sein oder können Elemente mehrfach darin existieren? – Pshemo

Antwort

3

Sie werden nicht angeben, was Sie, wenn einige b von der ersten Karte in der zweiten gibt es nicht tun möchte Karte, also ist dies möglicherweise nicht genau das, wonach Sie suchen.

mapAB.entrySet().stream() 
    .filter(e -> e.getValue().stream().anyMatch(mapBC::containsKey)) 
    .collect(toMap(
     Map.Entry::getKey, 
     e->e.getValue().stream() 
      .filter(mapBC::containsKey) 
      .map(mapBC::get) 
      .flatMap(Collection::stream) 
      .collect(toList()) 
)); 
+0

Ausgezeichneter Punkt. Ich werde das in der Frage klären. –

+0

Übrigens möchten Sie vielleicht erwähnen, dass der Code davon ausgeht, dass Sie 'Collectors.toMap()' und 'Collectors.toList()' 'statisch importiert haben. Ich nehme an, dass Sie auch "Map.Entry" importiert haben, so dass Sie auch das Präfix 'Map.' löschen können. –

+0

Ich wollte diese Antwort akzeptieren, aber dann schrieb ich einen Komponententest und entdeckte, dass, wenn ein Eintrag in mapAB auf eine Sammlung von Bs verweist, so dass keines dieser Bs Schlüssel in mapBC sind, Sie einen Eintrag in mapAC pointing haben zu einer leeren Sammlung. Das ist nicht das Ende der Welt, aber ich hatte gehofft, in diesem Fall einfach keinen Zugang zu haben. –

0

Wie wäre es damit:

Map<A, Collection<B>> mapAB = new HashMap<>(); 
    Map<B, Collection<C>> mapBC = new HashMap<>(); 
    Map<A, Collection<C>> mapAC = new HashMap<>(); 

    mapAB.entrySet().stream().forEach(a -> { 
     Collection<C> cs = new HashSet<>(); 
     a.getValue().stream().filter(b -> mapBC.containsKey(b)).forEach(b -> cs.addAll(mapBC.get(b))); 
     mapAC.put(a.getKey(), cs); 
    }); 
+0

Das ist definitiv kompakter als das Imperativ, das ich gerade zu der Frage hinzugefügt habe, aber es hat einen Fehler, den ich in meinem Beispiel auch anfangs nicht behandelt habe. Ihr Aufruf 'mapB.get (b)' (den Sie auf 'mapBC' aktualisieren könnten - siehe meine Benennungsupdates oben) könnte null zurückgeben, also müssen Sie damit umgehen. –

+0

Sie haben Recht, bitte überprüfen Sie meine Bearbeitungen ... (Filter hinzugefügt) –

+0

Danke für die Änderungen. Leider zeigt es immer noch das gleiche Verhalten wie Mischas Antwort, indem es leere Sammlungen für B erzeugt, die in mapBC keine Zuordnung haben. Siehe die Kommentare dieser Antwort für weitere Details. –

3

Ich bin kein Fan der forEach Ansatz, der umständlich unumgänglich ist. Ein reiner Ansatz könnte

mapAB.entrySet().stream() 
    .flatMap(
     entryAB -> entryAB.getValue().stream().flatMap(
      b -> mapBC.getOrDefault(b, Collections.<C>emptyList()) 
      .stream().map(
       c -> new AbstractMap.SimpleEntry<>(entryAB.getKey(), c)))) 
    // we now have a Stream<Entry<A, C>> 
    .groupingBy(
    Entry::getKey, 
    mapping(Entry::getValue, toList())); 

... oder vielleicht abwechselnd

mapA.entrySet().stream() 
    .flatMap(
     entryAB -> entryAB.getValue().stream().map(
      b -> new AbstractMap.SimpleEntry<>(
       entryAB.getKey(), 
       mapBC.getOrDefault(b, Collections.<C>emptyList())))) 
    // we now have a Stream<Entry<A, Collection<C>>> 
    .groupingBy(
    Entry::getKey, 
    mapping(Entry::getValue, 
     reducing(
      Collections.<C>emptyList(), 
      (cs1, cs2) -> { 
      List<C> merged = new ArrayList<>(cs1); 
      merged.addAll(cs2); 
      return merged; 
      }))); 
+0

Ich denke über deine Antwort nach. Die zweite Version scheint clever, aber ein bisschen schwer zu interpretieren. Übrigens habe ich den Karten Namen gegeben (mapAB, mapBC). Ich würde Ihre Antwort bearbeiten, um mapAB zu verwenden, aber es beinhaltet nur einige wenige Zeichen zu ändern, die StackOverflow mir nicht erlaubt. –

+1

Aktualisiert für die Variablennamen sowie Ihre Angabe, dass 'mapBC' möglicherweise nicht alle Bs enthält, die in' mapAB' erscheinen. –

0
Map<A, Collection<C>> mapC = 
    mapA.entrySet().stream().collect(Collectors.toMap(
     entry -> entry.getKey(), 
     entry -> entry.getValue().stream().flatMap(b -> mapB.get(b).stream()) 
      .collect(Collectors.toSet()))); 

Fühlen Sie sich frei Collectors.toSet() mit toList() zu ersetzen oder sogar toCollection().

+0

Ich mag die Klarheit in diesem Ansatz, aber sehe meine Frage Updates oben. 'mapB.get (b)' könnte null zurückgeben, also müssen Sie damit umgehen. Siehe auch meine aktualisierte Variablennamen. –

0

Ich habe eigentlich nichts gegen den zwingenden Ansatz. Da Sie es in den Speicher sammeln, können Sie mit lambdas wirklich nichts anfangen, es sei denn, sie führen zu einem saubereren Code. Hier ist der Imperativ Ansatz ist gut so:

Map<A, Collection<C>> mapAC = new HashMap<>(); 

for (A key : mapAB.keySet()) { 
    Collection<C> cs = new HashSet<>(); 
    mapAC.put(key, cs); 

    for (B b : mapAP.get(key)) { 
     cs.addAll(mapBC.get(b)==null ? Collections.emptyList() : mapBC.get(b)); 
    } 
} 

obwohl ich in ausgekleideten Ihre if-Anweisung als ternärer Operator, und ich denke, mit den Tasten in dem for-Schleife klarer sieht.

+0

mapAB und mapBC könnten am Ende ziemlich groß sein, also würde ich lieber zusätzliche Lookups vermeiden, wenn möglich. Es ist eine Schande, dass Java [Groovys Elvis-Operator] (http://www.groovy-lang.org/operators.html#_elvis_operator) nicht hat, da dies perfekt als Ersatz für Ihren ternären Ausdruck wäre, während das Vermeiden des doppelte Suche –

1

Meine StreamEx Bibliothek bietet eine EntryStream Klasse, die Stream von Map.Entry Objekte mit einigen zusätzlichen bequemen Operationen ist.Dies ist, wie ich dieses Problem mit meiner Bibliothek lösen würde:

Map<A, Collection<C>> mapAC = EntryStream.of(mapAB) 
    .flatMapValues(Collection::stream) // flatten values: now elements are Entry<A, B> 
    .mapValues(mapBC::get) // map only values: now elements are Entry<A, Collection<C>> 
    .nonNullValues() // remove entries with null values 
    .flatMapValues(Collection::stream) // flatten values again: now we have Entry<A, C> 
    .groupingTo(HashSet::new); // group them to Map using HashSet as value collections 

Dies ist wahrscheinlich weniger effizient als eine hervorragende Lösung von @Misha als mehrere Zwischen erstellte Objekte erstellt werden, aber meiner Meinung nach ist es einfacher, auf diese Weise zu schreiben und zu verstehen, .