Совокупный список объектов в Java
Есть ли у нас какая-либо функция агрегатора в Java для выполнения приведенной ниже агрегации?
Person {
String name;
String subject;
String department;
Long mark1;
Long mark2;
Long mark3;
}
Список содержит данные, как показано ниже.
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
Критерии агрегации - это Subject и Dep. Результирующий объект должен быть
Subject |Department |Mark1 |Mark2 |Mark3
----------- |-----------|-------|-------|-----
English |DEP1 |13 |12 |13
Maths |DEP2 |3 |5 |6
Maths |DEP1 |9 |7 |8
Это агрегирование может быть достигнуто путем ручного перебора по списку и создания агрегированного списка. Пример, как показано ниже.
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());
}
Но есть ли какая-либо функция или функция агрегации Java 8, которую мы можем использовать?
Ответы
Ответ 1
Используя сборщики стандартов в JDK, вы можете сделать это следующим образом (предполагая создание класса Tuple3<E1, E2, E3>
):
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)))));
Это сначала сгруппирует элементы по их предмету, затем по отделу и уменьшит результирующие значения во второй карте, суммируя их метки.
Запустив его в списке лиц, которые у вас есть в вашем примере, вы получите результат:
Maths => DEP2 => (3, 5, 6)
Maths => DEP1 => (9, 7, 8)
English => DEP1 => (13, 12, 13)
В этом случае вы также можете использовать другой вариант с помощью коллектора toMap
. Логика остается неизменной, функция для сопоставления значений создаст карту, содержащую департамент в качестве ключа, и оценку учащегося как ценность. Функция слияния будет отвечать за добавление или обновление сопоставлений.
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;
}
));
Конечно, вы можете расспросить себя о "красоте" этих решений, возможно, вы хотите ввести пользовательский сборщик или пользовательские классы, чтобы сделать цель более понятной.
Ответ 2
Вы можете использовать reduction. Образец для агрегирования метки 1 выглядит следующим образом.
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);
}
}
По-прежнему ищет генерирование вывода через отдельные функции. Будет обновляться после завершения.
Ответ 3
Используя подход из Группируйте по нескольким именам полей в java 8 с пользовательским классом ключей, мое предложение таково:
Map<DepSubject, Grades> map = persons.stream().
collect(Collectors.groupingBy(x -> new DepSubject(x.department, x.subject),
Collectors.reducing(
new Grades(0, 0, 0),
y -> new Grades(y.mark1, y.mark2, y.mark3),
(x, y) -> new Grades(x.m1 + y.m1, x.m2 + y.m2, x.m3 + y.m3)
)));
DepSubject
определяет equals
и hashCode
. Таким образом, исходный класс не должен изменяться, и если требуются несколько критериев группировки, можно использовать несколько классов. К сожалению, это может быть довольно многословным в Java, поскольку вам нужен класс с equals, hashCode, (getters, seters). На самом деле, на мой взгляд, геттеры и сеттеры также могут быть опущены, если класс используется только в одном месте для группировки.
class DepSubject{
String department;
String subject;
public DepSubject(String department, String subject) {
this.department = department;
this.subject = subject;
}
public String getDepartment() {
return department;
}
// equals,hashCode must also be defined for this to work, omitted for brevity
}
Также можно собрать результаты в List. Таким образом, пользовательские классы DepSubject
и Grades
используются только для промежуточных операций:
List<Person> list = persons.stream().
collect(Collectors.collectingAndThen(
Collectors.groupingBy(x -> new DepSubject(x.department, x.subject),
Collectors.reducing(
new Grades(0, 0, 0),
y -> new Grades(y.mark1, y.mark2, y.mark3),
(x, y) -> new Grades(x.m1 + y.m1, x.m2 + y.m2, x.m3 + y.m3)
)),
map -> map.entrySet().stream()
.map(e -> new Person(null, e.getKey().subject, e.getKey().department, e.getValue().m1, e.getValue().m2, e.getValue().m3))
.collect(Collectors.toList())
));
Вы также можете извлечь логику groupingBy в функцию:
private static <T> List<Person> groupBy(List<Person> persons, Function<Person,T> function, BiFunction<T,Grades,Person> biFunction) {
return persons.stream().
collect(Collectors.collectingAndThen(
Collectors.groupingBy(function,
Collectors.reducing(
new Grades(0, 0, 0),
y -> new Grades(y.mark1, y.mark2, y.mark3),
(x, y) -> new Grades(x.m1 + y.m1, x.m2 + y.m2, x.m3 + y.m3)
)),
map -> map.entrySet().stream()
.map(e -> biFunction.apply(e.getKey(),e.getValue()))
.collect(Collectors.toList())
));
}
Таким образом, вы можете группировать своих людей таким образом:
List<Person> list = groupBy(persons,
x -> new DepSubject(x.department, x.subject),
(depSubject,grades) -> new Person(null, depSubject.subject, depSubject.department, grades.m1, grades.m2, grades.m3));
Если вы хотите группировать свой объект только по теме, вы можете просто сделать:
List<Person> list2 = groupBy(persons,
Person::getSubject,
(subject,grades) -> new Person(null,subject, null, grades.m1, grades.m2, grades.m3));