2015-02-13 17 views
18

Mi è stato presentato un problema interessante da un mio collega e non sono riuscito a trovare una soluzione Java 8 pulita e carina. Il problema è per lo streaming attraverso una lista di POJO e poi raccoglierli in una mappa basata su più proprietà - la mappatura fa sì che il POJO a verificarsi più volteJava 8 Stream: mappare lo stesso oggetto più volte in base a proprietà diverse

Immaginate la seguente POJO:

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 in su come List<Customer>:

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

alternativa 1: Utilizzare un Map al di fuori del "flusso" (o meglio, al di fuori forEach).

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

Alternativa 2: Creare voci della mappa e lo streaming, quindi flatMap loro. IMO è un po 'troppo prolisso e non è così facile da leggere.

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

Alternativa 3: Questo è un altro uno che mi si avvicinò con il codice "più bella" finora ma utilizza la versione a tre arg della reduce e il terzo parametro è un po 'dubbia come si trova in questo domanda: Purpose of third argument to 'reduce' function in Java 8 functional programming. Inoltre, reduce non sembra adatto per questo problema poiché è in muting e gli stream paralleli potrebbero non funzionare con l'approccio seguente.

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

Se il codice di cui sopra è stampato in questo modo:

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

Il risultato sarebbe:

{Super = clienti (Super Mac), Johnny = cliente (Johnny Puma) , Mac = Cliente (Super Mac), Puma = Cliente (Johnny Puma)}
{Super = Cliente (Super Mac), Johnny = Cliente (Johnny Puma), Mac = Cliente (Super Mac), Puma = Cliente (Johnny Puma)}
{ Super = cliente (Super Mac), Johnny = cliente (Johnny Puma), Mac = cliente (Super Mac), Puma = cliente (Johnny Puma)}

Così, ora alla mia domanda: come dovrei, in un modo ordinato di Java 8, scorrere attraverso lo List<Customer> e quindi in qualche modo raccoglierlo come Map<String, Customer> in cui è stato diviso il tutto come due chiavi (first E last) ovvero lo Customer viene mappato due volte. Non voglio usare alcuna libreria di terze parti, non voglio usare una mappa al di fuori del flusso come in alt 1. Ci sono altre alternative carine?

Il codice completo può essere found on hastebin per la semplice copia-incolla per ottenere il tutto in esecuzione.

risposta

18

penso che le alternative 2 e 3 possono essere riscritti per essere più chiari:

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

alternativi 3: I vostri abusi codice reduce mutando la HashMap . Per fare la riduzione mutevole, utilizzare collect:

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

Si noti che questi non sono identici. L'alternativa 2 genererà un'eccezione se ci sono chiavi duplicate mentre Alternative 3 sovrascriverà silenziosamente le voci.

Se sovrascrivere le voci in caso di chiavi duplicate è quello che desideri, preferirei personalmente l'alternativa 3. Mi è subito chiaro che cosa fa. Assomiglia più strettamente alla soluzione iterativa. Mi aspetto che sia più performante visto che l'alternativa 2 deve fare un sacco di allocazioni per cliente con tutto quel flatmapping.

Tuttavia, l'Alternativa 2 ha un enorme vantaggio rispetto all'Alternativa 3 separando la produzione di voci dalla loro aggregazione. Questo ti dà una grande flessibilità. Ad esempio, se si desidera modificare Alternativa 2 per sovrascrivere le voci su chiavi duplicate anziché generare un'eccezione, è sufficiente aggiungere (a,b) -> b a toMap(...). Se si decide di raccogliere le voci corrispondenti in un elenco, tutto ciò che si deve fare è sostituire toMap(...) con groupingBy(...), ecc.

+5

Non è solo che 'collect()' è "il modo preferito"; usare 'reduce()' per questo è semplicemente sbagliato. Ridurre è per valori; le contorsioni che attraversa l'Alternativa 3 viola le specifiche di 'reduce()'. E se più tardi proverai a eseguire quel flusso in parallelo, otterrai la risposta sbagliata. D'altra parte, usando 'collect()' come Misha mostra è progettato per gestire questo caso ed è sicuro in sequenza o in parallelo. –

+2

@BrianGoetz Grazie, Brian. Ho modificato la mia risposta per essere più energica proprio come hai postato il tuo commento. – Misha

Problemi correlati