Search code examples
javajava-streamcollectorsreduction

Collectors.reducing method is updating the same identity when used as downstream for Collectors.partitionBy


I have a class similar to the below MyObject.

public class MyObject {
    private String key; // not unique. multiple objects can have the same key.
    private boolean isPermanent;
    private double value1;
    private double value2;
    
    public MyObject merge(MyObject m){
        value1 += m.getValue1();
        value2 += m.getValue2();
        return this;
    }

    // getters, setters and constructors...
}

Following is the sample data:

List<MyObject> objs = new ArrayList<>();

objs.add(new MyObject("1", false, 100, 200));
objs.add(new MyObject("2", false, 300, 100));
objs.add(new MyObject("1", false, 200, 300));

objs.add(new MyObject("3", true, 100, 200));
objs.add(new MyObject("1", true, 500, 100));
objs.add(new MyObject("1", true, 100, 100));

I want to combine these objects based on isPermanent and key and did the following:

(Note that I have added import static java.util.stream.Collectors.* to import groupingBy, partitioningBy and reducing in the below code)

objs.stream()
    .collect(partitioningBy(MyObject::isPermanent,
                            groupingBy(MyObject::getKey,
                                       reducing(new MyObject(), MyObject::merge))));

The returned map is of type Map<Boolean, Map<String, MyObject>>. I am expecting the map returned to be as below (ignoring fields other than value1 and value2)

{false : { 1 : { 300, 500 } } , { 2 : { 300, 100 } }, true : { 1 : { 600, 200 } } , { 3 : { 100, 200 } } }

But the result I am getting is like:

{false : { 1 : { 1300, 1000 } } , { 2 : { 1300, 1000 } }, true : { 1 : { 1300, 1000 } } , { 3 : { 1300, 1000 } } }

Since I am passing an object as the identity, I believe that the same object is being updated for every group. Since a lambda cannot be passed onto the reduction method, is there any way to solve this problem?


Solution

  • You can use a static merge function that returns a new instance:

    public static MyObject merge(MyObject one,MyObject two) {
        MyObject merged = new MyObject ();
        merged.setValue1(one.getValue1()+two.getValue1());
        merged.setValue2(one.getValue2()+two.getValue2());
        return merged;
    }
    

    You'll have to remove your existing non-static merge method in order for the static method to be picked by the compiler in place of the method reference.

    This way, MyObject::merge will produce a new MyObject instance each time.

    If you don't want to remove the existing method, you can still add the static method if you replace the method reference with the following lambda expression:

    (o1,o2)->MyObject.merge(o1,o2)
    

    Without adding the static method, you can replace the MyObject::merge method reference with the following lambda expression:

    (o1,o2)-> new MyObject().merge(o1).merge(o2)