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.
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. –
@BrianGoetz Grazie, Brian. Ho modificato la mia risposta per essere più energica proprio come hai postato il tuo commento. – Misha