2015-06-19 28 views
7

Mam dwie mapy:Korzystanie Java 8 lambdas/transformacje połączyć i spłaszczyć dwie mapy

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

chciałbym, aby przekształcić je w Map<A, Collection<C>> mapAC i jestem zastanawiasz się, czy jest to gładki sposób na lambda i transformacje. W moim przypadku kolekcje są kompletami, ale chciałbym rozwiązać problem ogólnie dla kolekcji.

Jedna myśl, jaką miałem, to najpierw połączyć dwie mapy w Map<A, Map<B, Collection<C>>>, a następnie spłaszczyć, ale jestem otwarty na każde podejście.

noty danych: B powinien występować jedynie w zbiorze wartości związanej z jednym A i to samo jest prawdą dla mapBC (dane są odwzorowywane C tylko z jednego B). W rezultacie powinna istnieć tylko jedna ścieżka od danego A do danego C, chociaż mogą istnieć odwzorowania, dla których nie ma odwzorowań i mogą istnieć odwzorowania , dla których nie ma odpowiednich odwzorowań A -> B. Te sieroty po prostu nie pojawiają się w wynikowym mapAC.

Dla porównania, oto przykład czysto bezwzględnej podejścia do tego samego problemu:

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

Czy chcesz połączyć dwie relacje jeden-do-wielu bez środkowej kolumny? –

+0

@MikeSamuel tak, to zdecydowanie jeden sposób, aby na to spojrzeć. –

+1

Czy możesz dodać przykład danych? Na przykład, jeśli mamy 'Mapę >' i 'Map >' czy jest możliwe, aby różne osoby miały tę samą pracę, lub dla kilku zadań używały tych samych narzędzi? Czy możliwe jest coś takiego jak 'p1 -> {j1, j2}, p2 -> {j2, j3}'? Również 'job1 -> {tool1, tool2} job2 -> {tool2, tool3} job3 -> {tool4}'? Jakie wyniki oczekujesz? Czy chcesz też, aby 'Collection ' było 'Set' lub czy elementy istnieją w nim wiele razy? – Pshemo

Odpowiedz

3

Nie określić, co chcesz zrobić, jeśli niektóre b od pierwszej mapy nie istnieją w sekundę mapę, więc może to nie być dokładnie to, czego szukasz.

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

Doskonały punkt. Wyjaśnię to w pytaniu. –

+0

Przy okazji, możesz wspomnieć, że kod zakłada, że ​​statycznie zaimportowałeś 'Collectors.toMap()' oraz 'Collectors.toList()'. Z przyjemnością zakładam, że zaimportowałeś również plik 'Map.Entry', więc możesz również usunąć prefiks' Map'. –

+0

Chciałem zaakceptować tę odpowiedź, ale potem napisałem test jednostkowy i odkryłem, że jeśli wpis w mapAB wskazuje na kolekcję Bs w taki sposób, że żadna z tych Bs nie jest kluczem w mapie mapBC, to kończy się pozycją w mapAC wskazując do pustej kolekcji. To nie koniec świata, ale miałem nadzieję, że w tym przypadku nie będę miał żadnego wpisu. –

0

Jak o tym:

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

Jest to zdecydowanie bardziej kompaktowy niż imperatywowy odpowiednik, który właśnie dodałem do pytania, ale ma jedną wadę, z którą początkowo nie miałem do czynienia w moim przykładzie. Twoje polecenie 'mapB.get (b)' (które możesz zaktualizować do 'mapBC' - zobacz moje aktualizacje nazw powyżej) może zwrócić wartość zerową, więc musisz sobie z tym poradzić. –

+0

Masz rację, przejrzyj moje zmiany ... (dodano filtr) –

+0

Dzięki za zmiany. Niestety, wciąż wykazuje to samo zachowanie, co odpowiedź Mishy, ​​tworząc puste kolekcje dla B, które nie mają mapowania w mapBC. Zobacz komentarze do odpowiedzi, aby uzyskać więcej informacji. –

3

Nie jestem fanem forEach podejście, które jest niezręcznie konieczne. Czystszej podejście może być

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

... a może na przemian

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

Zastanawiam się nad twoją odpowiedzią. Druga wersja wydaje się sprytna, ale nieco trudna do zinterpretowania. Przy okazji, podałem nazwy map (mapAB, mapBC). Zmodyfikuję twoją odpowiedź, aby używać mapAB, ale wymaga to tylko zmiany kilku znaków, których StackOverflow mi na to nie pozwoli. –

+1

Zaktualizowano nazwy zmiennych oraz wskazanie, że 'mapBC' może nie zawierać wszystkich B, które pojawiają się w' mapAB'. –

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

Zapraszam do zastąpienia Collectors.toSet() z toList() lub nawet toCollection().

+0

Podoba mi się klarowność w tym podejściu, ale zobacz moje aktualizacje pytań powyżej. 'mapB.get (b)' może zwrócić wartość null, więc będziesz musiał sobie z tym poradzić. Zobacz też moją zaktualizowaną nazwę zmiennej. –

0

Nie mam nic przeciwko imperatywnemu podejściu. Od kiedy zbieracie go do pamięci, nie ma nic do zyskania przez używanie lambdas, chyba że prowadzą one do czystszego kodu. Tutaj konieczne podejście jest po prostu w porządku:

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

chociaż ustawiły się w państwa if jako operatora potrójnego i myślę, że za pomocą klawiszy w pętli for wygląda jaśniej.

+0

mapAB i mapBC może okazać się całkiem olbrzymia, więc wolałbym unikać dodatkowych wyszukiwań, jeśli to możliwe. Szkoda, że ​​Java nie ma operatora [Groovy's Elvis] (http://www.groovy-lang.org/operators.html#_elvis_operator), ponieważ byłby idealny jako substytut twojego potrójnego wyrażenia, unikając jednocześnie podwójne wyszukiwanie. –

1

Moja biblioteka StreamEx zapewnia klasę EntryStream, która jest strumieniem obiektów Map.Entry z dodatkowymi wygodnymi operacjami.To w jaki sposób mogę rozwiązać ten problem za pomocą mojego Biblioteka:

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 

to prawdopodobnie mniej skuteczny jako doskonałe rozwiązanie dostarczone przez @Misha jak więcej obiektów pośrednich są tworzone, ale moim zdaniem łatwiej jest pisać i rozumieć w ten sposób .

Powiązane problemy