2015-02-13 3 views
18

Ich wurde von einem Kollegen von mir mit einem interessanten Problem vorgestellt und ich konnte keine ordentliche und schöne Java 8 Lösung finden. Das Problem ist durch eine Liste von POJOs zu streamen und sie dann in einer Karte basierend auf mehreren Eigenschaften sammeln - die Abbildung bewirkt, dass die POJO mehrere MaleJava 8 Streams: Map das gleiche Objekt mehrmals basierend auf verschiedenen Eigenschaften

Stellen Sie sich folgende POJO auftreten:

private static class Customer { 
    public String first; 
    public String last; 

    public Customer(String first, String last) { 
     this.first = first; 
     this.last = last; 
    } 

    public String toString() { 
     return "Customer(" + first + " " + last + ")"; 
    } 
} 

Set it up als List<Customer>:

// The list of customers 
List<Customer> customers = Arrays.asList(
     new Customer("Johnny", "Puma"), 
     new Customer("Super", "Mac")); 

Alternative 1: eine Map außerhalb des "Strom" (oder eher außerhalb forEach) verwenden.

// Alt 1: not pretty since the resulting map is "outside" of 
// the stream. If parallel streams are used it must be 
// ConcurrentHashMap 
Map<String, Customer> res1 = new HashMap<>(); 
customers.stream().forEach(c -> { 
    res1.put(c.first, c); 
    res1.put(c.last, c); 
}); 

Alternative 2: Map-Einträge erstellen und streamen, dann flatMap sie. IMO ist es ein bisschen zu ausführlich und nicht so leicht zu lesen.

// Alt 2: A bit verbose and "new AbstractMap.SimpleEntry" feels as 
// a "hard" dependency to AbstractMap 
Map<String, Customer> res2 = 
     customers.stream() 
       .map(p -> { 
        Map.Entry<String, Customer> firstEntry = new AbstractMap.SimpleEntry<>(p.first, p); 
        Map.Entry<String, Customer> lastEntry = new AbstractMap.SimpleEntry<>(p.last, p); 
        return Stream.of(firstEntry, lastEntry); 
       }) 
       .flatMap(Function.identity()) 
       .collect(Collectors.toMap(
         Map.Entry::getKey, Map.Entry::getValue)); 

Alternative 3: Dies ist eine andere, die ich bisher mit dem „schönsten“ Code kam, aber es nutzt die drei argument Version von reduce und der dritte Parameter ist ein wenig schmuddelig wie in dieser gefunden Frage: Purpose of third argument to 'reduce' function in Java 8 functional programming. Darüber hinaus scheint reduce für dieses Problem nicht gut geeignet zu sein, da es mutiert und parallele Streams möglicherweise nicht mit dem folgenden Ansatz funktionieren.

// Alt 3: using reduce. Not so pretty 
Map<String, Customer> res3 = customers.stream().reduce(
     new HashMap<>(), 
     (m, p) -> { 
      m.put(p.first, p); 
      m.put(p.last, p); 
      return m; 
     }, (m1, m2) -> m2 /* <- NOT USED UNLESS PARALLEL */); 

Wenn der obige Code wie folgt gedruckt wird:

System.out.println(res1); 
System.out.println(res2); 
System.out.println(res3); 

Das Ergebnis wäre:

{Super-= Kunden (Super Mac), Johnny = Kunde (Johnny Puma) , Mac = Kunde (Super Mac), Puma = Kunde (Johnny Puma)}
{Super = Kunde (Super Mac), Johnny = Kunde (Johnny Puma), Mac = Kunde (Super Mac), Puma = Kunde (Johnny Puma)}
{ Super = Kunden (Super Mac), Johnny = Kunde (Johnny Puma), Mac = Kunden (Super Mac), Puma = Kunde (Johnny Puma)}

So, nun zu meiner Frage: Wie sollte ich, in einer Java 8 geordneten Mode, streamen durch die List<Customer> und dann irgendwie sammeln sie als Map<String, Customer>, wo Sie das ganze Ding als zwei Schlüssel (first AND last), dh die Customer ist zweimal zugeordnet. Ich möchte keine Bibliotheken von Drittanbietern verwenden. Ich möchte keine Karte außerhalb des Streams wie in Alt 1 verwenden. Gibt es noch andere nette Alternativen?

Der vollständige Code kann found on hastebin für einfache Kopie-einfügen, um die ganze Sache zu laufen.

Antwort

18

Ich denke, Ihre Alternativen 2 und 3 kann neu geschrieben werden, um mehr klar:

Alternative 2:

Map<String, Customer> res2 = customers.stream() 
    .flatMap(
     c -> Stream.of(c.first, c.last) 
     .map(k -> new AbstractMap.SimpleImmutableEntry<>(k, c)) 
    ).collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); 

Alternative 3: Ihr Code Mißbräuche reduce durch die HashMap mutiert . Gehen Sie wandelbar Reduktion verwenden collect:

Map<String, Customer> res3 = customers.stream() 
    .collect(
     HashMap::new, 
     (m,c) -> {m.put(c.first, c); m.put(c.last, c);}, 
     HashMap::putAll 
    ); 

Beachten Sie, dass diese nicht identisch sind. Alternative 2 wird eine Ausnahme auslösen, wenn doppelte Schlüssel vorhanden sind, während Alternative 3 die Einträge im Hintergrund überschreibt.

Wenn das Überschreiben von Einträgen im Fall von doppelten Schlüsseln ist was Sie wollen, würde ich persönlich Alternative 3 bevorzugen. Es ist mir sofort klar, was es tut. Es ähnelt am ehesten der iterativen Lösung. Ich würde erwarten, dass es leistungsfähiger wird, da Alternative 2 mit all dem Flatmapping pro Kunde eine Menge Zuweisungen vornehmen muss.

Allerdings hat Alternative 2 einen großen Vorteil gegenüber Alternative 3, indem sie die Produktion von Einträgen von ihrer Aggregation trennt. Das gibt Ihnen viel Flexibilität. Wenn Sie beispielsweise Alternative 2 so ändern möchten, dass Einträge für doppelte Schlüssel überschrieben werden, statt eine Ausnahme auszulösen, fügen Sie einfach (a,b) -> b zu toMap(...) hinzu. Wenn Sie beschließen, übereinstimmende Einträge in einer Liste zu sammeln, müssen Sie nur toMap(...) durch groupingBy(...), usw. ersetzen.

+5

Es ist nicht nur das 'collect()' ist "der bevorzugte Weg"; Die Verwendung von 'reduce()' ist einfach falsch. Reduzieren ist für Werte; Die Verzerrungen, die die Alternative 3 durchläuft, verletzen die Spezifikation von 'reduce()'. Und wenn Sie später versuchen, diesen Stream parallel zu starten, erhalten Sie nur die falsche Antwort. Auf der anderen Seite ist die Verwendung von 'collect()', wie Misha zeigt, für diesen Fall ausgelegt und ist entweder sequentiell oder parallel sicher. –

+2

@BrianGoetz Danke, Brian. Ich habe meine Antwort modifiziert, um noch kraftvoller zu sein, als du deinen Kommentar gepostet hast. – Misha