使用Java流以不变的方式更改数据 [英] Change data in an immutable way with Java stream

查看:113
本文介绍了使用Java流以不变的方式更改数据的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

考虑以下代码:

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

基本上我正在做的是使用func1更新HashMap,使用func2应用第二种转换并将第二次更新的值保存在列表中. 我以不变的方式生成了所有新对象newData和list.

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.

我的问题: 可以一次流式传输原始HashMap(数据)吗?

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

我尝试过:

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.

推荐答案

这似乎是JDK 12中即将推出的Collectors.teeing方法的理想用例.这是

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收集到两个不同的收集器,然后将两个部分结果合并为最终结果.对于最后一步,我使用JDK 9的

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.

对于第一个收集器,我正在使用您的确切代码收集到Map,而对于第二个收集器,我正在使用 Collectors.toList ,使用

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.

编辑:如果您不能等到JDK 12发布,则可以同时使用此代码:

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

即使使用两个流,您的解决方案也绝对可以.我上面的解决方案仅对原始映射进行一次流传输,但是将func1应用于所有值两次.如果func1昂贵,您可以考虑对其进行记忆(即,缓存其结果,以便每当使用相同的输入再次调用它时,都从缓存中返回结果,而不是再次对其进行计算).或者,您也可以先将func1应用于原始地图的值,然后使用Collectors.teeing进行收集.

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

现在,您可以使用此记忆化的func1,它将完全像以前一样工作,除了使用先前使用过的参数调用apply方法时,它将从缓存中返回结果.

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.

另一种解决方案是先应用func1然后收集:

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

同样,我正在使用jdk9的Map.entry(K k, V v)静态方法.

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

这篇关于使用Java流以不变的方式更改数据的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆