使用Java流以不变的方式更改数据

giu*_*eri 4 java java-8 java-stream

考虑以下代码:

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());
Run Code Online (Sandbox Code Playgroud)

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

我的问题:是否可以将原始HashMap(数据)流一次?

我尝试了这个:

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;
    }));    
Run Code Online (Sandbox Code Playgroud)

但是这样做会在列表更新中产生副作用,因此我失去了“不变方式”的要求。

Fed*_*ner 5

这似乎是Collectors.teeingJDK 12中即将推出的方法的理想用例。这是webrev,这是CSR。您可以按以下方式使用它:

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));
Run Code Online (Sandbox Code Playgroud)

Collectors.teeing收集到两个不同的收集器,然后将两个部分结果合并为最终结果。对于最后一步,我使用的是JDK 9的Map.entry(K k, V v)静态方法,但是我可以使用任何其他容器,例如PairTuple2,等等。

对于第一集我用你的确切代码,以收集到Map,而第二集我使用的Collectors.mapping一起Collectors.toList使用Function.andThen来编写func1func2函数映射步骤。


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

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);
}
Run Code Online (Sandbox Code Playgroud)

注意:创建返回的收集器时,不考虑下游收集器的特性(作为练习)。


编辑2:即使使用两个流,您的解决方案也完全可以。我上面的解决方案只对原始地图进行一次流式处理,但func1对所有值都应用两次。如果func1价格昂贵,则可以考虑对其进行记忆(即缓存其结果,以便每当使用相同的输入再次调用它时,都从缓存中返回结果,而不是再次对其进行计算)。或者,您也可以先应用func1原始地图的值,然后使用进行收集Collectors.teeing

记忆很容易。只需声明此实用程序方法:

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);
}
Run Code Online (Sandbox Code Playgroud)

然后按如下方式使用它:

Function<BigDecimal, BigDecimal> func1 = memoize(x -> x); //This could be anything
Run Code Online (Sandbox Code Playgroud)

现在,您可以使用该备注func1,它可以像以前一样工作,除了apply使用先前使用过的参数调用其方法时,它将从缓存中返回结果。

另一种解决方案是先申请func1然后收集:

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));
Run Code Online (Sandbox Code Playgroud)

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