我有一个列表fooList
class Foo {
private String category;
private int amount;
private int price;
... constructor, getters & setters
}
我想按类别分组,然后将金额以及价格相加。
结果将存储在地图中:
Map<Foo, List<Foo>> map = new HashMap<>();
键是Foo,它具有汇总的数量和价格,并列出具有相同类别的所有对象的值。
到目前为止,我已经尝试了以下方法:
Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getCategory()));
现在,我只需要用持有汇总金额和价格的Foo对象替换String键即可。这就是我卡住的地方。我似乎找不到任何方法。
答案 0 :(得分:15)
有点丑陋,但是应该可以工作:
list.stream().collect(Collectors.groupingBy(Foo::getCategory))
.entrySet().stream()
.collect(Collectors.toMap(x -> {
int sumAmount = x.getValue().stream().mapToInt(Foo::getAmount).sum();
int sumPrice= x.getValue().stream().mapToInt(Foo::getPrice).sum();
return new Foo(x.getKey(), sumAmount, sumPrice);
}, Map.Entry::getValue));
答案 1 :(得分:4)
我的Sweepers answer的变体使用精简的收集器,而不是两次进行流传输以对各个字段求和:
Map<Foo, List<Foo>> map = fooList.stream()
.collect(Collectors.groupingBy(Foo::getCategory))
.entrySet().stream()
.collect(Collectors.toMap(e -> e.getValue().stream().collect(
Collectors.reducing((l, r) -> new Foo(l.getCategory(),
l.getAmount() + r.getAmount(),
l.getPrice() + r.getPrice())))
.get(),
e -> e.getValue()));
它并不是真正的更好,因为它会创建很多短暂的Foos
。
请注意,Foo
必须提供hashCode
-和equals
实现,这些实现只考虑category
才能使生成的map
正常工作。一般来说,这可能不是您想要Foo
所需要的。我希望定义一个单独的FooSummary
类来包含聚合数据。
答案 2 :(得分:3)
如果您有一个特殊的专用构造函数,并且在hashCode
中一致实现的equals
和Foo
方法如下:
public Foo(Foo that) { // not a copy constructor!!!
this.category = that.category;
this.amount = 0;
this.price = 0;
}
public int hashCode() {
return Objects.hashCode(category);
}
public boolean equals(Object another) {
if (another == this) return true;
if (!(another instanceof Foo)) return false;
Foo that = (Foo) another;
return Objects.equals(this.category, that.category);
}
上面的hashCode
和equals
实现使您可以将Foo
用作地图中的有意义键(否则地图将被破坏)。
现在,借助Foo
中的新方法,该方法可以同时聚合amount
和price
属性,您可以分两步执行所需的操作。首先是方法:
public void aggregate(Foo that) {
this.amount += that.amount;
this.price += that.price;
}
现在是最终解决方案:
Map<Foo, List<Foo>> result = fooList.stream().collect(
Collectors.collectingAndThen(
Collectors.groupingBy(Foo::new), // works: special ctor, hashCode & equals
m -> { m.forEach((k, v) -> v.forEach(k::aggregate)); return m; }));
编辑:缺少一些观察结果...
一方面,此解决方案迫使您使用hashCode
和equals
的实现,如果它们属于相同的Foo
,则认为这两个不同的category
实例是相等的。也许这是不希望的,或者您已经具有将更多或其他属性考虑在内的实现。
另一方面,使用Foo
作为映射的键(用于按实例的属性之一对实例进行分组)是非常不常见的用例。我认为最好使用category
属性按类别分组并具有两个映射:Map<String, List<Foo>>
保持分组,Map<String, Foo>
保持聚合的price
和amount
,两种情况下的密钥均为category
。
此外,此解决方案在将条目放入地图后对地图的键进行突变。这很危险,因为这可能会破坏地图。但是,在这里,我仅对Foo
和hashCode
或equals
Foo
既不参与的属性进行突变。我认为,由于要求的特殊性,这种风险在这种情况下是可以接受的。
答案 3 :(得分:1)
我建议您创建一个帮助类,其中包含金额和价格
final class Pair {
final int amount;
final int price;
Pair(int amount, int price) {
this.amount = amount;
this.price = price;
}
}
然后只收集要映射的列表:
List<Foo> list =//....;
Map<Foo, Pair> categotyPrise = list.stream().collect(Collectors.toMap(foo -> foo,
foo -> new Pair(foo.getAmount(), foo.getPrice()),
(o, n) -> new Pair(o.amount + n.amount, o.price + n.price)));
答案 4 :(得分:0)
我的解决方案:)
public static void main(String[] args) {
List<Foo> foos = new ArrayList<>();
foos.add(new Foo("A", 1, 10));
foos.add(new Foo("A", 2, 10));
foos.add(new Foo("A", 3, 10));
foos.add(new Foo("B", 1, 10));
foos.add(new Foo("C", 1, 10));
foos.add(new Foo("C", 5, 10));
List<Foo> summarized = new ArrayList<>();
Map<Foo, List<Foo>> collect = foos.stream().collect(Collectors.groupingBy(new Function<Foo, Foo>() {
@Override
public Foo apply(Foo t) {
Optional<Foo> fOpt = summarized.stream().filter(e -> e.getCategory().equals(t.getCategory())).findFirst();
Foo f;
if (!fOpt.isPresent()) {
f = new Foo(t.getCategory(), 0, 0);
summarized.add(f);
} else {
f = fOpt.get();
}
f.setAmount(f.getAmount() + t.getAmount());
f.setPrice(f.getPrice() + t.getPrice());
return f;
}
}));
System.out.println(collect);
}