Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reduce and assign using Stream api for multiple variables

I need to do following in lambda but unable to think of way where single stream and reducing(s) can help :(

I have Employee ArrayList where name member variable can be duplicate across object in list. I need to create map where key is employee name and value is object where we have sum of cost1 and cost2 for that employee.

Employee class has -> String name, Integer cost1, Integer cost2

Output class has -> Integer cost1, Integer cost2

List <Employee> employeeList = new ArrayList<>();
// data population in inputMap
Map<String, Output> outputMap = new HashMap<>();
for (Employee emp : employeeList)
{
    Output op = outputMap.get(emp.getName());
    if(op == null){
      Output newOp = new Output ();
      newOp.setCost1(emp.getCost1())
      newOp.setCost2(emp.getCost2())
      newOp.put(emp.getName(), newOp);
    }else{
       int cost1 = op.getCost1() + emp.getCost1();
       int cost2 = op.getCost2() + emp.getCost2();
       op.setCost1(cost1);
       op.setCost2(cost2);
       newOp.put(emp.getName(), op);
    }
}
like image 272
KRS Avatar asked Dec 10 '25 12:12

KRS


2 Answers

I assume you have a structure sort of like this:

static class Output {
    private final int cost1;

    private final int cost2;

    public Output(int cost1, int cost2) {
        this.cost1 = cost1;
        this.cost2 = cost2;
    } 

    @Override
    public String toString() {
        return "cost1 = " + cost1 + " cost2 = " + cost2;
    }
    // ... getter
}  

static class Employee {
    private final String name;

    private final int cost1;

    private final int cost2;

    public Employee(String name, int cost1, int cost2) {
        this.name = name;
        this.cost1 = cost1;
        this.cost2 = cost2;
    }
 // ...getters      
}

Then the solution would be to first group by Employee::getName and reduce to Output via Collectors.reducing

  Map<String, Output> map = Arrays.asList(
                       new Employee("e", 12, 12), 
                       new Employee("f", 13, 13), 
                       new Employee("e", 11, 11))
            .stream()
            .collect(Collectors.groupingBy(Employee::getName,
                    Collectors.reducing(
                            new Output(0, 0),
                            emp -> new Output(emp.getCost1(), emp.getCost2()),
                            (left, right) -> new Output(left.getCost1() + right.getCost1(), left.getCost2() + right.getCost2()))));
    System.out.println(map); // {e=cost1 = 23 cost2 = 23, f=cost1 = 13 cost2 = 13}
like image 104
Eugene Avatar answered Dec 12 '25 01:12

Eugene


Whenever you need to get a reduced map out of the elements of your stream, you can use the three-argument version of Collectors.toMap:

Map<String, Output> result = employeeList.stream()
    .collect(Collectors.toMap(
        Employee::getName,
        employee -> new Output(employee.getCost1(), employee.getCost2()),
        (left, right) -> {
            left.setCost1(left.getCost1() + right.getCost1());
            left.setCost2(left.getCost2() + right.getCost2());
            return left;
        }));

Collectors.toMap, as its name suggests, collects the elements of the stream to a map. Its first argument is a function that converts elements of the stream to keys, the second argument is another function that converts elements of the stream to values, and the third argument is a merge function that is applied to values when there's a collision on the keys.

In this case, I'm using a mutable reduction on the left operand of the merge function. This is to avoid creating too many instances of the Output class. It is safe to reduce this way because in the value function (the second argument of Collectors.toMap) I'm creating a new instance of the Output class.

This solution could be greatly improved if you could add the following constructor and method to the Output class:

// creates an Output instance out of an Employee instance
public Output(Employee employee) {
    this.cost1 = employee.getCost1();
    this.cost2 = employee.getCost2();
}

// merges another Output instance into this Output instance
public Output merge(Output another) {
    this.cost1 += another.cost1;
    this.cost2 += another.cost2;
    return this;
}

Now, collecting to the map would be much simpler:

Map<String, Output> result = employeeList.stream()
    .collect(Collectors.toMap(Employee::getName, Output::new, Output::merge));
like image 38
fps Avatar answered Dec 12 '25 02:12

fps



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!