Q:

Aggregate List of objects in Java

Do we have any aggregator function in Java to perform the below aggregation?
Person {
    String name;
    String subject;
    String department;
    Long mark1;
    Long mark2;
    Long mark3;
}

List contains data as below.

Name    |Subject    |Department |Mark1  |Mark2  |Mark3
--------|-----------|-----------|-------|-------|-----
Clark   |English    |DEP1       |7      |8      |6
Michel  |English    |DEP1       |6      |4      |7
Dave    |Maths      |DEP2       |3      |5      |6
Mario   |Maths      |DEP1       |9      |7      |8

The aggregation criteria is Subject & Dep. The resultant object needs to be

Subject     |Department |Mark1  |Mark2  |Mark3
----------- |-----------|-------|-------|-----
English     |DEP1       |13     |12     |13
Maths       |DEP2       |3      |5      |6
Maths       |DEP1       |9      |7      |8

This aggregation can be achieved by manually iterating through the list and create an aggregated list. Example as below.
private static List<Person> getGrouped(List<Person> origList) {
    Map<String, Person> grpMap = new HashMap<String, Person>();

    for (Person person : origList) {
        String key = person.getDepartment() + person.getSubject();
        if (grpMap.containsKey(key)) {
            Person grpdPerson = grpMap.get(key);
            grpdPerson.setMark1(grpdPerson.getMark1() + person.getMark1());
            grpdPerson.setMark2(grpdPerson.getMark2() + person.getMark2());
            grpdPerson.setMark3(grpdPerson.getMark3() + person.getMark3());
        } else {
            grpMap.put(key, person);
        }
    }
    return new ArrayList<Person>(grpMap.values());
}

But is there any aggregation function or feature of Java 8 which we can leverage?

A:

You can use reduction. Sample to aggregate mark1 is as follows.
public class Test {

    static class Person {
        Person(String name, String subject, String department, Long mark1, Long mark2, Long mark3) {
            this.name = name;
            this.subject = subject;
            this.department = department;
            this.mark1 = mark1;
            this.mark2 = mark2;
            this.mark3= mark3;
        }
            String name;
            String subject;
            String department;
            Long mark1;
            Long mark2;
            Long mark3;

            String group() {
                return subject+department;
            }

            Long getMark1() {
                return mark1;
            }
    }

      public static void main(String[] args)
      {
        List<Person> list = new ArrayList<Test.Person>();
        list.add(new Test.Person("Clark","English","DEP1",7l,8l,6l));
        list.add(new Test.Person("Michel","English","DEP1",6l,4l,7l));
        list.add(new Test.Person("Dave","Maths","DEP2",3l,5l,6l));
        list.add(new Test.Person("Mario","Maths","DEP1",9l,7l,8l));

        Map<String, Long> groups = list.stream().collect(Collectors.groupingBy(Person::group, Collectors.reducing(
                    0l, Person::getMark1, Long::sum)));

        //Or alternatively as suggested by Holger 
        Map<String, Long> groupsNew = list.stream().collect(Collectors.groupingBy(Person::group, Collectors.summingLong(Person::getMark1)));

        System.out.println(groups);

      }

}

Still looking into generating the output via a single functions. Will update once completed.

A:

Using standards collectors in the JDK, you can do it like this (assuming the creation of a Tuple3<E1, E2, E3> class):
Map<String, Map<String, Tuple3<Long, Long, Long>>> res =
    persons.stream().collect(groupingBy(p -> p.subject,
                                        groupingBy(p -> p.department,
                                                   reducing(new Tuple3<>(0L, 0L, 0L), 
                                                            p -> new Tuple3<>(p.mark1, p.mark2, p.mark3), 
                                                            (t1, t2) -> new Tuple3<>(t1.e1 + t2.e1, t1.e2 + t2.e2, t1.e3 + t2.e3)))));

This will first group the elements by their subject, then by department and reduces the resulting values in the second map by summing their marks.
Running it on the list of persons you have in your example, you'll get as output:
Maths => DEP2 => (3, 5, 6)
Maths => DEP1 => (9, 7, 8)
English => DEP1 => (13, 12, 13)

In this case you may also want to use another variant using the toMap collector. The logic remains the same, the function to map the values will create a map containing the department as a key, and the grade of the student as a value. The merge function will be in charge to add or update the mappings.
Map<String, Map<String, Tuple3<Long, Long, Long>>> res3 =
        persons.stream()
               .collect(toMap(p -> p.subject,
                              p -> {
                                  Map<String, Tuple3<Long, Long, Long>> value = new HashMap<>();
                                  value.put(p.department, new Tuple3<>(p.mark1, p.mark2, p.mark3));
                                  return value;
                              },
                              (v1, v2) -> {
                                   v2.forEach((k, v) -> v1.merge(k, v, (t1, t2) -> new Tuple3<>(t1.e1 + t2.e1, t1.e2 + t2.e2, t1.e3 + t2.e3)));
                                   return v1;
                              }
               ));

Of course you can question yourself about the "beauty" of these solutions, maybe you want to introduce a custom collector or custom classes to make the intent more clear.

