Java中list集合根据字段汇总金额

新手上路,请多包涵

我有一个Foo类

class Foo {
    private String id;
    private String currency;
    private BigDecima amount;

... constructor, getters & setters

现在有一个List集合中存有Foo类中的字段。
现在需要相同的id和currency上的amount相加。汇总成一个新的List集合。
最好使用lambda表达式

阅读 5.8k
2 个回答

为什么非要 lambda

        // 原始 list
        List<Foo> l = new ArrayList<>();
        l.add(new Foo("1", "0.1", new BigDecimal("0.1")));
        l.add(new Foo("2", "0.2", new BigDecimal("0.2")));
        l.add(new Foo("1", "0.1", new BigDecimal("0.2")));
        l.add(new Foo("3", "0.3", new BigDecimal("0.5")));
        l.add(new Foo("2", "0.2", new BigDecimal("0.3")));

        List<Foo> r = new ArrayList<>();
        l.forEach(f -> {
            if (r.stream().anyMatch(c -> f.id.equals(c.id) && f.currency.equals(c.currency))) {
                return;
            }
            l.stream().filter(c -> f != c && f.id.equals(c.id) && f.currency.equals(c.currency))
                .forEach(c -> f.amount = f.amount.add(c.amount));
            r.add(f);
        });
        // 结果
        System.out.println(r);

既然题主提到说要用lambda表达式,而lambda本质来说就是一种写法,用stream的内部循环和传统的外部循环来比较,那就是写法不一样,写法不一样那就更应该明确当前需要的输入和输出了,输入是给清楚了,但是输出呢?题主只是说要返回一个新的List集合,这个List里装的啥?不同的东西最终可能导致写法的不太一致,没准思考方式也不太一样哈

我暂且以最终还是返回List<Foo>为例,做个参考。


如果你想直接看最后的答案,可以直接拖到最后,下面只是我的一个思考过程而已


因为这里需要处理的主要问题就是相同的idcurrency上的amount相加,那归根到底就是相同的idcurrencyFoo对象要放在一起处理,这就是归类,归类在streamapi当然就要用Collectors.groupBy方法了

Collectors.groupBy有三个重载方法

  • groupingBy(Function classifier)
  • groupingBy(Function classifier, Collector downstream)
  • groupingBy(Function classifier, Supplier mapFactory, Collector downstream)

我把泛型去掉方便好看,其实参数也就是三种

  • classifier:根据什么方式去做分类,这是一个Function
  • mapFactory:最终分类的数据以什么类型map存放,因为分类嘛,就是相同类型的对象在一起,所以最终肯定是一个map结构
  • downstream:同一类的对象应该什么方式去做收集或者叫聚合

为了方便理解这三个参数,我们如果直接使用Collectors.groupingBy(Foo::getId)最后得到的就是Map<String, List<Foo>>

List<Foo> fooList = new ArrayList<>();
Map<String, List<Foo>> map = fooList.stream().collect(Collectors.groupingBy(Foo::getId));

我们可以看一下一个参数的Collectors.groupingBy是怎么实现的
image.png

它调用的是两个参数的方法,并且给downstream参数传递的是常用的toList()方法

这两个参数的方法又是调用的三个参数的方法
image.png

所以好理解为啥是最终得到Map<String, List<Foo>>,有了这个准备的基础我们再来看题主的问题

需要按照idcurrency分类, 那我们就创建一个分类的方式

fooList.stream()
       .collect(Collectors.groupingBy(foo -> foo.getId() + "_" + foo.getCurrency()))

这样写不太好看,我们可以把这个分类方式放在Foo

@Getter
@Setter
@AllArgsConstructor
public class Foo {
    private String id;
    private String currency;
    private BigDecimal amount;

    public String groupKey() {
        return this.id + "_" + this.currency;
    }
}

那我们就可以按照方法引用的方式写成

Map<String, List<Foo>> map = fooList.stream()
                                    .collect(Collectors.groupingBy(Foo::groupKey));                             

接下来我们考虑如何合并这些同类Foo对象的amount值,由于我们本身来说最终的结果不需要对于Collectors.groupingBy方法的第二个参数mapFactory做过多定制,所以我们就只采用两个参数的方法了

剩下这个参数downstream需要去处理amount的问题,相当于我们现在已经按照Foo::groupKey的方式做了归类,我们拿到了同一类的一堆Foo对象

  • 如果我们downstream参数写Collectors.toList,那最终结果肯定是Map<String, List<Foo>>
  • 如果我们downstream参数写Collectors.toSet,那最终结果肯定是Map<String, Set<Foo>>

但是这些方式不是我们想要的,我们不想要把这一堆的Foo对象转换为Set或者List,我们是希望把他们合并在一起,也就是

一堆Foo -> 一个Foo

那对应streamapi当然就要用Collectors.reducing方法了,这个reducing就是聚合的方法,把多个聚合成一个,正是我们想要的

reducing方法也有三个参数,不过比较稍微复杂一点点,我之前也有个回答提到了java8中3个参数的reduce方法怎么理解? 你可以去看一下,虽然里面说的是stream.reduce,但是和Collectors.reducing是一样的

总得来说就是处理两个相同的Foo如何合并为一个Foo,也就是提供reducing方法参数里的一种处理方式BinaryOperator

当然我们也可以把这种处理方式写到Foo类中,新增merge方法

@Getter
@Setter
@AllArgsConstructor
public class Foo {
    private String id;
    private String currency;
    private BigDecimal amount;

    public String groupKey() {
        return this.id + "_" + this.currency;
    }
    
    public Foo merge(Foo foo) {
        return new Foo(this.id, this.currency, this.amount.add(foo.getAmount()));
    }
}

结合之前的groupKey的写法,现在我们这个分类代码就可以写为

fooList.stream()
       .collect(Collectors.groupingBy(Foo::groupKey, Collectors.reducing(Foo::merge)));

看似现在可以了,其实还不行,因为此时返回的map的格式其实是Map<String, Optional<Foo>>
为啥mapvalueOptional呢,其实主要就是因为我们使用了reducing的一个参数的方法,只是指明了如何合并多个对象,但是万一这个stream是空的,所以reducing对应一个参数的方法返回的就是Optional

但是我们是先分类,然后再相同类中做reducing,所以只要有分类,reducing一定不会为空,所以这个Optional一定是有值的,那么我们是可以直接get出来的,不需要Optional的保护

相同于我们做完了reducing操作之后,还需要再做一个get操作,那这个对应streamapi当然就要用Collectors.collectingAndThen方法了

从方法名就可以看得出来,这个方法就是先收集然后再做其他操作,跟我们的需求就是吻合的,看方法签名

collectingAndThen(Collector downstream, Function finisher)

也是两个参数,第一个参数就是如何收集Collector,第二个就是转换的Function

根据我们之间的思考,第一个Collector参数就是Collectors.reducing(Foo::merge),那第二个参数Function显然也是我们之前说的get,也就是Optional::get

于是我们分类的代码就可以变为

fooList.stream()
       .collect(Collectors.groupingBy(
               Foo::groupKey, 
               Collectors.collectingAndThen(
                        Collectors.reducing(Foo::merge), 
                        Optional::get)))

此时最终返回的就是Map<String, Foo>,因为最终我们要的只是里面的values,所以我们再加上mapvalues()方法即可

Collection<Foo> values = fooList.stream()
                                .collect(Collectors.groupingBy(
                                      Foo::groupKey,
                                      Collectors.collectingAndThen(
                                               Collectors.reducing(Foo::merge),
                                               Optional::get)))
                                 .values();

但是注意此时返回的是Collection,不是List,如果题主不纠结这个的话,这样就可以了,但是非要转换为List只有不用values()方法而是再次循环一次Map<String, Foo>

List<Foo> values = fooList.stream()
                          .collect(Collectors.groupingBy(
                                 Foo::groupKey,
                                 Collectors.collectingAndThen(
                                          Collectors.reducing(Foo::merge),
                                          Optional::get)))
                          .entrySet()
                          .stream()
                          .map(Map.Entry::getValue)
                          .collect(Collectors.toList());

所以总结一下答案:

基础Foo

@Getter
@Setter
@AllArgsConstructor
public class Foo {
    private String id;
    private String currency;
    private BigDecimal amount;

    public String groupKey() {
        return this.id + "_" + this.currency;
    }
    
    public Foo merge(Foo foo) {
        return new Foo(this.id, this.currency, this.amount.add(foo.getAmount()));
    }
}
1. 多出一次循环的方案(groupingBy+collectingAndThen+reducing)
List<Foo> values = fooList.stream()
                          .collect(Collectors.groupingBy(
                                 Foo::groupKey,
                                 Collectors.collectingAndThen(
                                          Collectors.reducing(Foo::merge),
                                          Optional::get)))
                          .entrySet()
                          .stream()
                          .map(Map.Entry::getValue)
                          .collect(Collectors.toList());
2. 只有一次循环的方案(定制Collector)

因为上一个方案各种groupingBy+collectingAndThen+reducing其实他们返回的都是一个Collector,由于jdk给的Collector实现不一定完全满足我们的需求,所以不用jdk自带的,我们定制一个Collector也是可以的,之前也小写了一个文章Java8 collector接口的定制实现介绍,可以去看看,看完了再理解我写的,你会发现其实这种方案也挺简单的

public class FooCollectorImpl implements Collector<Foo, List<Foo>, List<Foo>> {

    private Map<String, Integer> map = new HashMap<>();

    @Override
    public Supplier<List<Foo>> supplier() {
        return ArrayList::new;
    }

    @Override
    public BiConsumer<List<Foo>, Foo> accumulator() {
        return (fooList, foo) -> {
            String key = foo.groupKey();
            Integer index = map.get(key);
            if (index == null) {
                fooList.add(foo);
                map.put(key, fooList.size() - 1);
            }else {
                Foo innerFoo = fooList.get(index);
                innerFoo = innerFoo.merge(foo);
                fooList.add(index, innerFoo);
            }
        };
    }

    @Override
    public BinaryOperator<List<Foo>> combiner() {
        return (fooList, fooList2) -> fooList;
    }

    @Override
    public Function<List<Foo>, List<Foo>> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
    }
}

因为定制的Collector已经把很多逻辑实现了,所以调用的时候显得非常简单

List<Foo> values = fooList.stream().collect(new FooCollectorImpl());

以上,仅供参考~╭( ̄▽ ̄)╯

推荐问题
宣传栏