4 votes

Modifier les données de manière immuable avec le flux Java

Considérez ce code :

Function<BigDecimal,BigDecimal> func1 = x -> x;//This could be anything
Function<BigDecimal,BigDecimal> func2 = y -> y;//This could be anything
Map<Integer,BigDecimal> data = new HashMap<>();

Map<Integer,BigDecimal> newData = 
    data.entrySet().stream().
        collect(Collectors.toMap(Entry::getKey,i -> 
            func1.apply(i.getValue())));

List<BigDecimal> list = 
    newData.entrySet().stream().map(i -> 
        func2.apply(i.getValue())).collect(Collectors.toList());

En gros, ce que je fais est de mettre à jour un HashMap avec func1, d'appliquer une deuxième transformation avec func2 et de sauvegarder la deuxième valeur mise à jour dans une liste. J'ai fait tout cela de manière immuable en générant les nouveaux objets newData et list.

MA QUESTION : Est-il possible de faire ce streaming de la HashMap originale (données) une fois ?

J'ai essayé ça :

Function<BigDecimal,BigDecimal> func1 = x -> x;
Function<BigDecimal,BigDecimal> func2 = y -> y;
Map<Integer,BigDecimal> data = new HashMap<>();
List<BigDecimal> list = new ArrayList<>();

Map<Integer,BigDecimal> newData = 
    data.entrySet().stream().collect(Collectors.toMap(
        Entry::getKey,i -> 
        {
            BigDecimal newValue = func1.apply(i.getValue());
            //SIDE EFFECT!!!!!!!
            list.add(func2.apply(newValue));
            return newValue;
    }));    

mais en faisant cela, j'ai un effet secondaire sur la mise à jour de la liste et j'ai perdu l'exigence de "méthode immuable".

5voto

Cela semble être un cas d'utilisation idéal pour la future Collectors.teeing dans le JDK 12. Voici la webrev et voici le CSR . Vous pouvez l'utiliser comme suit :

Map.Entry<Map<Integer, BigDecimal>, List<BigDecimal>> result = data.entrySet().stream()
    .collect(Collectors.teeing(
             Collectors.toMap(
                     Map.Entry::getKey, 
                     i -> func1.apply(i.getValue())),
             Collectors.mapping(
                     i -> func1.andThen(func2).apply(i.getValue()),
                     Collectors.toList()),
             Map::entry));

Collectors.teeing collecte à deux collecteurs différents, puis fusionne les deux résultats partiels dans le résultat final. Pour cette étape finale, j'utilise la fonction de collecte de JDK 9 Map.entry(K k, V v) mais j'aurais pu utiliser n'importe quel autre conteneur, à savoir Pair o Tuple2 etc.

Pour le premier collecteur, j'utilise votre code exact pour collecter vers un Map tandis que pour le second collecteur, j'utilise Collectors.mapping ainsi que Collectors.toList en utilisant Function.andThen pour composer votre func1 y func2 pour l'étape de mise en correspondance.


EDITAR: Si vous ne pouvez pas attendre la sortie du JDK 12, vous pouvez utiliser ce code en attendant :

public static <T, A1, A2, R1, R2, R> Collector<T, ?, R> teeing(
        Collector<? super T, A1, R1> downstream1,
        Collector<? super T, A2, R2> downstream2,
        BiFunction<? super R1, ? super R2, R> merger) {

    class Acc {
        A1 acc1 = downstream1.supplier().get();
        A2 acc2 = downstream2.supplier().get();

        void accumulate(T t) {
            downstream1.accumulator().accept(acc1, t);
            downstream2.accumulator().accept(acc2, t);
        }

        Acc combine(Acc other) {
            acc1 = downstream1.combiner().apply(acc1, other.acc1);
            acc2 = downstream2.combiner().apply(acc2, other.acc2);
            return this;
        }

        R applyMerger() {
            R1 r1 = downstream1.finisher().apply(acc1);
            R2 r2 = downstream2.finisher().apply(acc2);
            return merger.apply(r1, r2);
        }
    }

    return Collector.of(Acc::new, Acc::accumulate, Acc::combine, Acc::applyMerger);
}

Remarque : les caractéristiques des collecteurs en aval ne sont pas prises en compte lors de la création du collecteur retourné (laissé à titre d'exercice).


EDIT 2 : Votre solution est tout à fait correcte, même si elle utilise deux flux. Ma solution ci-dessus ne diffuse la carte originale qu'une seule fois, mais elle applique func1 à toutes les valeurs deux fois. Si func1 est coûteux, vous pouvez envisager mémorisation (c'est-à-dire en mettant en cache ses résultats, de sorte qu'à chaque fois qu'elle est appelée à nouveau avec la même entrée, vous renvoyez le résultat du cache au lieu de le calculer à nouveau). Vous pouvez également commencer par appliquer func1 aux valeurs de la carte originale, puis de collecter avec Collectors.teeing .

La mémorisation est facile. Il suffit de déclarer cette méthode utilitaire :

public <T, R> Function<T, R> memoize(Function<T, R> f) {
    Map<T, R> cache = new HashMap<>(); // or ConcurrentHashMap
    return k -> cache.computeIfAbsent(k, f);
}

Et ensuite, utilisez-le comme suit :

Function<BigDecimal, BigDecimal> func1 = memoize(x -> x); //This could be anything

Maintenant vous pouvez utiliser ce mémo func1 et il fonctionnera exactement comme avant, sauf qu'il renverra les résultats du cache lorsque son apply est invoquée avec un argument qui a été utilisé précédemment.

L'autre solution serait d'appliquer func1 d'abord et ensuite de collecter :

Map.Entry<Map<Integer, BigDecimal>, List<BigDecimal>> result = data.entrySet().stream()
    .map(i -> Map.entry(i.getKey(), func1.apply(i.getValue())))
    .collect(Collectors.teeing(
             Collectors.toMap(
                     Map.Entry::getKey, 
                     Map.Entry::getValue),
             Collectors.mapping(
                     i -> func2.apply(i.getValue()),
                     Collectors.toList()),
             Map::entry));

Encore une fois, j'utilise l'interface de jdk9. Map.entry(K k, V v) méthode statique.

0voto

talex Points 7172

Votre code peut être simplifié de cette façon :

    List<BigDecimal> list = data.values().stream()
            .map(func1)
            .map(func2)
            .collect(Collectors.toList());

0voto

Nikolas Points 11342

Votre objectif est d'appliquer ces fonctions à tous les éléments suivants BigDecimal dans les Map . Vous pouvez obtenir toutes ces valeurs à partir de la carte en utilisant Map::values qui renvoie le List . Ensuite, appliquez le flux à la liste uniquement. Considérez le data contient déjà quelques entrées :

List<BigDecimal> list = data.values().stream()
                                     .map(func1)
                                     .map(func2)
                                     .collect(Collectors.toList()); 

Je vous déconseille d'itérer toutes les entrées ( Set<Entry<Integer, BigDecimal>> ) puisque vous ne devez travailler qu'avec les valeurs.

0voto

Nonika Points 1090

Essayez de cette façon, il retourne un tableau d'objets [2], le premier est la carte et le second est la liste.

Map<Integer, BigDecimal> data = new HashMap<>();

        data.put(1, BigDecimal.valueOf(30));
        data.put(2, BigDecimal.valueOf(40));
        data.put(3, BigDecimal.valueOf(50));

        Function<BigDecimal, BigDecimal> func1 = x -> x.add(BigDecimal.valueOf(10));//This could be anything
        Function<BigDecimal, BigDecimal> func2 = y -> y.add(BigDecimal.valueOf(-20));//This could be anything

        Object[] o = data.entrySet().stream()
                .map(AbstractMap.SimpleEntry::new)
                .map(entry -> {
                    entry.setValue(func1.apply(entry.getValue()));
                    return entry;
                })
                .collect(Collectors.collectingAndThen(toMap(Map.Entry::getKey, Map.Entry::getValue), a -> {
                    List<BigDecimal> bigDecimals = a.values().stream().map(func2).collect(Collectors.toList());
                    return new Object[]{a,bigDecimals};
                }));
        System.out.println(data);
        System.out.println((Map<Integer, BigDecimal>)o[0]);
        System.out.println((List<BigDecimal>)o[1]);

Sortie :

 Original Map: {1=30, 2=40, 3=50}
 func1 map: {1=40, 2=50, 3=60}
 func1+func2 list: [20, 30, 40]

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X