4

Consider this 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());

Basically what I'm doing is updating an HashMap with func1,to apply a second trasformation with func2 and to save second time updated value in a list. I DID all in immutable way generating the new objects newData and list.

MY QUESTION: It is possible to do that streaming the original HashMap (data) once?

I tried this:

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

but doing so I have a side effect in list updating so I lost the 'immutable way' requirement.

fps
  • 33,623
  • 8
  • 55
  • 110

4 Answers4

5

This seems like an ideal use case for the upcoming Collectors.teeing method in JDK 12. Here's the webrev and here's the CSR. You can use it as follows:

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 collects to two different collectors and then merges both partial results into the final result. For this final step I'm using JDK 9's Map.entry(K k, V v) static method, but I could have used any other container, i.e. Pair or Tuple2, etc.

For the first collector I'm using your exact code to collect to a Map, while for the second collector I'm using Collectors.mapping along with Collectors.toList, using Function.andThen to compose your func1 and func2 functions for the mapping step.


EDIT: If you cannot wait until JDK 12 is released, you could use this code meanwhile:

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

Note: The characteristics of the downstream collectors are not considered when creating the returned collector (left as an exercise).


EDIT 2: Your solution is absolutely OK, even though it uses two streams. My solution above streams the original map only once, but it applies func1 to all the values twice. If func1 is expensive, you might consider memoizing it (i.e. caching its results, so that whenever it's called again with the same input, you return the result from the cache instead of computing it again). Or you might also first apply func1 to the values of the original map, and then collect with Collectors.teeing.

Memoizing is easy. Just declare this utility method:

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

And then use it as follows:

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

Now you can use this memoized func1 and it will work exactly as before, except that it will return results from the cache when its apply method is invoked with an argument that has been previously used.

The other solution would be to apply func1 first and then collect:

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

Again, I'm using jdk9's Map.entry(K k, V v) static method.

fps
  • 33,623
  • 8
  • 55
  • 110
  • 1
    Thanks for sharing the update. Just couldn't compare the complexity brought in here as compared to [the other solution.](https://stackoverflow.com/a/53098210/1746118) ... maybe the question is interpreted incorrectly there? – Naman Nov 01 '18 at 13:22
  • @nullpointer The things is that OP wants *both* a new map with only `func1` applied to the values and also a list with `func2 ∘ func1` applied to them. – fps Nov 01 '18 at 13:31
  • 1
    Hmm, now I get the choice of merge function and the return type of your solution. Interesting. – Naman Nov 01 '18 at 13:55
0

Your code can be simplified this way:

    List<BigDecimal> list = data.values().stream()
            .map(func1)
            .map(func2)
            .collect(Collectors.toList());
talex
  • 17,973
  • 3
  • 29
  • 66
0

Your goal is to apply these functions to all the BigDecimal values in the Map. You can get all these values from the map using Map::values which returns the List. Then apply the Stream to the List only. Consider the data already contains some entries:

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

I discourage you from iterating all the entries (Set<Entry<Integer, BigDecimal>>) since you only need to work with the values.

Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183
0

Try this way it returns Array of Object[2] the first one is the map and second one is the list

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

Output:

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