user3203030
user3203030

Reputation: 75

Java 8 : Convert List<ObjectA> into List<ObjectB> by grouping and summing over different fields using streams

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

Answers (2)

fps
fps

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

Manos Nikolaidis
Manos Nikolaidis

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

Related Questions