web-dev-qa-db-fra.com

Java 8 flux: mappez le même objet plusieurs fois en fonction de différentes propriétés

Un de mes collègues m'a présenté un problème intéressant et je n'ai pas pu trouver une solution soignée et jolie Java 8 solution. Le problème est de parcourir une liste de POJO, puis de les collecter dans une carte basée sur plusieurs propriétés - la cartographie fait que le POJO se produit plusieurs fois

Imaginez le POJO suivant:

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

Configurez-le en tant que List<Customer>:

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

Alternative 1 : Utilisez un Map en dehors du "stream" (ou plutôt en dehors de 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);
});

Alternative 2 : Créez des entrées de mappage et diffusez-les, puis flatMap. OMI c'est un peu trop verbeux et pas si facile à lire.

// 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 : Ceci est un autre que j'ai trouvé le code "le plus joli" jusqu'à présent, mais il utilise la version à trois arguments de reduce et le troisième paramètre est un peu douteux comme dans cette question: But du troisième argument pour réduire la fonction dans Java 8 programmation fonctionnelle . De plus, reduce ne semble pas convenir à ce problème car il mute et les flux parallèles peuvent ne pas fonctionner avec l'approche ci-dessous.

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

Si le code ci-dessus est imprimé comme ceci:

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

Le résultat serait:

{Super = Client (Super Mac), Johnny = Client (Johnny Puma), Mac = Client (Super Mac), Puma = Client (Johnny Puma)}
{Super = Client (Super Mac), Johnny = Client (Johnny Puma), Mac = Client (Super Mac), Puma = Client (Johnny Puma)}
{Super = Client (Super Mac), Johnny = Client (Johnny Puma), Mac = Client (Super Mac), Puma = Client (Johnny Puma)}

Donc, maintenant à ma question: comment dois-je, de manière ordonnée Java 8, diffuser à travers le List<Customer> puis en quelque sorte le récupérer en tant que Map<String, Customer> où vous divisez le tout en deux clés (first ET last) c'est-à-dire que le Customer est mappé deux fois. Je ne veux pas utiliser de bibliothèques tierces, je ne veux pas utiliser une carte en dehors du flux comme dans l'alt 1. Y a-t-il d'autres alternatives Nice?

Le code complet peut être trouvé sur hastebin pour un simple copier-coller pour faire fonctionner le tout.

25
wassgren

Je pense que vos alternatives 2 et 3 peuvent être réécrites pour être plus claires:

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: Votre code abuse reduce en mutant le HashMap. Pour effectuer une réduction modifiable, utilisez collect:

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

Notez que ceux-ci ne sont pas identiques. L'alternative 2 lèvera une exception s'il y a des clés en double tandis que l'alternative 3 écrasera silencieusement les entrées.

Si vous souhaitez écraser les entrées en cas de clés en double, je préférerais personnellement la variante 3. Je comprends immédiatement ce qu'elle fait. Elle ressemble le plus à la solution itérative. Je m'attendrais à ce qu'il soit plus performant car l'Alternative 2 doit faire un tas d'allocations par client avec tout ce flatmapping.

Cependant, la variante 2 a un énorme avantage sur la variante 3 en séparant la production des entrées de leur agrégation. Cela vous donne une grande flexibilité. Par exemple, si vous souhaitez modifier la variante 2 pour remplacer les entrées sur les clés en double au lieu de lever une exception, vous devez simplement ajouter (a,b) -> b À toMap(...). Si vous décidez de collecter les entrées correspondantes dans une liste, il vous suffit de remplacer toMap(...) par groupingBy(...), etc.

20
Misha