Flux Java 8: mapper 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 j'ai été incapable de trouver une solution Java 8 soignée et jolie. Le problème est de diffuser une liste de POJOs, puis de les collecter dans une carte basée sur plusieurs propriétés-le mappage provoque le POJO à se produire 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 comme 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 "flux" (ou plutôt en dehors 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 carte et diffusez-les, puis flatMap. IMO 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 : C'est un autre que j'ai trouvé avec le code "le plus joli" jusqu'à présent, mais il utilise la version à trois arg de reduce et le troisième paramètre est un peu douteux comme trouvé dans cette question: but du troisième argument pour "réduire" la fonction dans la programmation fonctionnelle Java 8. En outre, reduce ne semble pas être un bon ajustement pour ce problème car il est en mutation 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 devrais-je, D'une manière ordonnée Java 8, diffuser à travers le List<Customer> et ensuite le collecter 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 alt 1. Existe-il d'autres nice des solutions de rechange?

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

22
demandé sur Community 2015-02-13 23:31:23

1 réponses

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

Variante 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 : votre code abuse reduce en mutant le HashMap. Pour effectuer une réduction mutable, 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 lancera une exception s'il y a des clés en double tandis que L'Alternative 3 écrasera silencieusement les entrées.

Si l'écrasement des entrées en cas de clés en double est ce que vous voulez, je préférerais personnellement Alternative 3. Il est immédiatement clair pour moi ce qu'il fait. Il 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 présente un énorme avantage par rapport à 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 Alternative 2 pour écraser les entrées sur les clés en double au lieu de lancer une exception, vous ajoutez simplement (a,b) -> b à toMap(...). Si vous décidez de collecter des entrées correspondantes dans une liste, Tout ce que vous auriez à faire est de remplacer toMap(...) par groupingBy(...), etc.

19
répondu Misha 2015-02-13 22:48:54