Java 8 Lambda Collectors.summingLong multiple columns?
Asked Answered
F

4

9

I have POJO definition as follows:

class EmployeeDetails{
 private String deptName;
 private Double salary;
 private Double bonus;
 ...
}

Currently, i have lambda expression for Group By 'deptName' as :

$set.stream().collect(Collectors.groupingBy(EmployeeDetails::getDeptName,
                                 Collectors.summingLong(EmployeeDetails::getSalary));

Question Is it possible to Sum more than one column? I need to compute sum on both fields salary and bonus in one expression instead of multiple times?

SQL representation would be:

SELECT deptName,SUM(salary),SUM(bonus)
FROM TABLE_EMP
GROUP BY deptName;
Firewater answered 23/3, 2017 at 16:39 Comment(0)
M
7

You need to create an additional class that will hold your 2 summarised numbers (salary and bonus). And a custom collector.

Let's say you have

private static final class Summary {
    private double salarySum;
    private double bonusSum;

    public Summary() {
        this.salarySum = 0;
        this.bonusSum = 0;
    }

    @Override
    public String toString() {
        return "Summary{" +
                "salarySum=" + salarySum +
                ", bonusSum=" + bonusSum +
                '}';
    }
}

for holding sums. Then you need a collector like this:

private static class EmployeeDetailsSummaryCollector implements Collector<EmployeeDetails, Summary, Summary> {
    @Override
    public Supplier<Summary> supplier() {
        return Summary::new;
    }

    @Override
    public BiConsumer<Summary, EmployeeDetails> accumulator() {
        return (summary, employeeDetails) -> {
            summary.salarySum += employeeDetails.salary;
            summary.bonusSum += employeeDetails.bonus;
        };
    }

    @Override
    public BinaryOperator<Summary> combiner() {
        return (summary, summary1) -> {
            summary.salarySum += summary1.salarySum;
            summary.bonusSum += summary1.bonusSum;
            return summary;
        };
    }

    @Override
    public Function<Summary, Summary> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return EnumSet.of(Collector.Characteristics.IDENTITY_FINISH);
    }
}

With these classes you can collect your results like

final List<EmployeeDetails> employees = asList(
        new EmployeeDetails(/* deptName */"A", /* salary */ 100d, /* bonus */ 20d),
        new EmployeeDetails("A", 150d, 10d),
        new EmployeeDetails("B", 80d, 5d),
        new EmployeeDetails("C", 100d, 20d)
);

final Collector<EmployeeDetails, Summary, Summary> collector = new EmployeeDetailsSummaryCollector();
final Map<String, Summary> map = employees.stream()
        .collect(Collectors.groupingBy(o -> o.deptName, collector));
System.out.println("map = " + map);

Which prints this:

map = {A=[salary=250.0, bonus=30.0], B=[salary=80.0, bonus=5.0], C=[salary=100.0, bonus=20.0]}
Mensuration answered 23/3, 2017 at 17:22 Comment(0)
F
5

I know you've got your answer, but here is my take(I was writing while the other was posted). There is already a Pair in java in the form of AbstractMap.SimpleEntry.

 System.out.println(Stream.of(new EmployeeDetails("first", 50d, 7d), new EmployeeDetails("first", 50d, 7d),
            new EmployeeDetails("second", 51d, 8d), new EmployeeDetails("second", 51d, 8d))
            .collect(Collectors.toMap(EmployeeDetails::getDeptName,
                    ed -> new AbstractMap.SimpleEntry<>(ed.getSalary(), ed.getBonus()), 
                    (left, right) -> {
                        double key = left.getKey() + right.getKey();
                        double value = left.getValue() + right.getValue();
                        return new AbstractMap.SimpleEntry<>(key, value);
                    }, HashMap::new)));
Footstep answered 23/3, 2017 at 17:38 Comment(1)
This solution works great too! but limited to two columns right? if we need 3 or 4 column, complexity increases.Firewater
E
1

Grouping by is a terminal operation that yields a map. The map produced by the groupingBy in the code below is a Map<String, List<EmployeeDetails>>. I create a new stream using the Map entrySet method. I then create a new Map using Collectors.toMap. This approach uses method chaining to avoid creating another class and create more concise code.

details.stream()
    .collect(Collectors.groupingBy(EmployeeDetails::getDeptName))
    .entrySet()
    .stream()
    .collect(Collectors.toMap(x->x.getKey(), x->x.getValue()
        .stream()
        .mapToDouble(y -> y.getSalary() + y.getBonus())
        .sum()));
Edina answered 23/3, 2017 at 18:10 Comment(0)
E
0

The question targets specifically Java 8, but the time passes and the Java language improves. Long story short, you may try to use Collectors.teeing available since Java 12:

Map<String, Pair<Double, Double>> res =
    set.stream()
        .collect(
            Collectors.groupingBy(
                EmployeeDetails::getDeptName,
                Collectors.teeing(
                    Collectors.summingDouble(EmployeeDetails::getSalary),
                    Collectors.summingDouble(EmployeeDetails::getBonus),
                    Pair::of)));
Efren answered 19/8, 2023 at 20:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.