Reputation: 75
I'm trying to understand java 8 streams. I have two classes.
public class ObjectA {
private String fieldA;
private String fieldB;
private String fieldC;
private double valueA;
private double valueB;
private double valueC;
}
public class ObjectB {
private String fieldA;
private double valueA;
private double valueB;
private double valueC;
}
I am trying to convert List<ObjectA>
into List<ObjectB>
by grouping over fieldA
and summing valueA
, valueB
, valueC
using streams, but can't exactly figure out how to do it.
Here is what I want to do :
Map<String,ObjectB> fieldCObjectBMap = new HashMap<>();
for(ObjectA objectA : objectAList) {
if(fieldCObjectBMap.size != 0 && fieldCObjectBMap.keyset().contains(objectA.getFieldC)) {
ObjectB objectB = fieldCObjectBMap.get(objectA.getFieldC);
objectB.setValueA(objectB.getValueA()+objectA.getValueA());
objectB.setValueB(objectB.getValueB()+objectA.getValueB());
objectB.setValueC(objectB.getValueC()+objectA.getValueC());
fieldCObjectBMap.put(objectA.getFieldC,objectB);
} else {
ObjectB objectB = fieldCObjectBMap.get(objectA.getFieldC);
objectB.setValueA(objectA.getValueA());
objectB.setValueB(objectA.getValueB());
objectB.setValueC(objectA.getValueC());
fieldCObjectBMap.put(objectA.getFieldC,objectB);
}
}
List<ObjectB> objectBList = fieldCObjectBMap.values();
Upvotes: 2
Views: 358
Reputation: 34460
One way is using Collectors.toMap
as @Manos Nikolaidis did in his answer. Another way is to use a custom collector. I will do this inside a static helper method, using a local class Acc
to accumulate partial results:
static Collector<ObjectA, ?, ObjectB> summingFields() {
class Acc {
ObjectB b = new ObjectB();
void sum(ObjectA a) {
this.b.setFieldA(a.getFieldA());
this.b.setValueA(b.getValueA() + a.getValueA());
this.b.setValueB(b.getValueB() + a.getValueB());
this.b.setValueC(b.getValueC() + a.getValueC());
}
Acc merge(Acc other) {
this.b.setValueA(this.b.getValueA() + other.b.getValueA());
this.b.setValueB(this.b.getValueB() + other.b.getValueB());
this.b.setValueC(this.b.getValueC() + other.b.getValueC());
return this;
}
ObjectB getB() {
return this.b;
}
}
return Collector.of(Acc::new, Acc::sum, Acc::merge, Acc::getB);
}
This uses the method Collector.of
, which creates a custom collector based on its arguments. Then, you could use the summingFields
method to get a Collection<ObjectB>
as follows:
Collection<ObjectB> grouped = objectAList.stream()
.collect(Collectors.groupingBy(
ObjectA::getFieldA,
summingFields()))
.values();
However, everything would be much easier if you could add the following couple of methods in your ObjectB
class:
public void sum(ObjectA a) {
this.fieldA = a.getFieldA();
this.valueA += a.getValueA();
this.valueB += a.getValueB();
this.valueC += a.getValueC();
}
public ObjectB merge(ObjectB b) {
this.valueA += b.getValueA();
this.valueB += b.getValueB();
this.valueC += b.getValueC();
return this;
}
Then, you could use Collector.of
with these methods:
Collection<ObjectB> grouped = objectAList.stream()
.collect(Collectors.groupingBy(
ObjectA::getFieldA,
Collector.of(ObjectB::new, ObjectB::sum, ObjectB::merge)))
.values();
Upvotes: 1
Reputation: 22224
A suggestion for a converter from List<ObjectA>
to List<ObjectB>
with the rules you describe:
class AtoBCollector {
private static ObjectB makeFromA(ObjectA a) {
return new ObjectB(a.getFieldA(), a.getValueA(), a.getValueB(), a.getValueC());
}
private static BinaryOperator<ObjectB> reduceB = (b1, b2) ->
new ObjectB(b1.getFieldA(),
b1.getValueA() + b2.getValueA(),
b1.getValueB() + b2.getValueB(),
b1.getValueC() + b2.getValueC());
static List<ObjectB> collect(List<ObjectA> objectAList) {
return new ArrayList<>(objectAList.stream()
.map(AtoBCollector::makeFromA)
.collect(toMap(ObjectB::getFieldA, identity(), reduceB))
.values());
}
}
What toMap does is : The first time a value of fieldA
is encountered, it will put it to a Map<String, ObjectB>
as key with value the corresponding instance of ObjectB
. The next times it will merge two ObjectB
instances with reduceB
.
Note that values()
returns a Collection
and you have to explicitly create a List
. In many cases you can use the Collection
directly and that's preferable.
As alternative to the above you can inline makeFromA
and reduceB
and skip the definition of AtoBCollector
. That resulting code will be considered less readable by some people. Exercise your best judgement.
As another alternative, if the source code of ObjectA
and ObjectB
is under your control, you can implement the functionality of makeFromA
as a constructor of ObjectB
and/or reduceB
as a method of ObjectB
.
Upvotes: 2