web-dev-qa-db-fra.com

Liste agrégée d'objets en Java

Avons-nous une fonction d'agrégation en Java pour effectuer l'agrégation ci-dessous?

Person {
    String name;
    String subject;
    String department;
    Long mark1;
    Long mark2;
    Long mark3;
}

La liste contient les données ci-dessous.

 Nom | Sujet | Département | Mark1 | Mark2 | Mark3 
 -------- | ----------- | -------- --- | ------- | ------- | ----- 
 Clark | Anglais | DEP1 | 7 | 8 | 6 
 Michel | Anglais | DEP1 | 6 | 4 | 7 
 Dave | Maths | DEP2 | 3 | 5 | 6 
 Mario | Maths | DEP1 | 9 | 7 | 8 

Les critères d'agrégation sont Subject & Dep. L'objet résultant doit être

 Sujet | Département | Mark1 | Mark2 | Mark3 
 ----------- | ----------- | ------- | ------- | ----- 
 Anglais | DEP1 | 13 | 12 | 13 
 Maths | DEP2 | 3 | 5 | 6 
 Maths | DEP1 | 9 | 7 | 8 

Cette agrégation peut être réalisée en itérant manuellement dans la liste et en créant une liste agrégée. Exemple comme ci-dessous.

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());
}

Mais existe-t-il une fonction d'agrégation ou une fonctionnalité de Java 8 que nous pouvons exploiter?

13
Swadeesh

En utilisant des collecteurs de normes dans le JDK, vous pouvez le faire comme ceci (en supposant la création d'une classe 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)))));

Cela regroupera d’abord les éléments par sujet, puis par département et réduira les valeurs résultantes dans la deuxième carte en additionnant leurs marques.

En l'exécutant sur la liste des personnes que vous avez dans votre exemple, vous obtiendrez en sortie:

Maths => DEP2 => (3, 5, 6)
Maths => DEP1 => (9, 7, 8)
English => DEP1 => (13, 12, 13)

Dans ce cas, vous pouvez également utiliser une autre variante à l'aide du collecteur toMap. La logique reste la même, la fonction de mappage des valeurs créera un mappage contenant le département en tant que clé et la note de l'étudiant en tant que valeur. La fonction de fusion sera chargée d’ajouter ou de mettre à jour les mappages.

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;
                              }
               ));

Bien entendu, vous pouvez vous interroger sur la "beauté" de ces solutions. Peut-être souhaitez-vous introduire un collecteur personnalisé ou des classes personnalisées pour clarifier l'intention?.

3
Alexis C.

Vous pouvez utiliser reduction . L'échantillon pour agréger mark1 est comme suit.

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);

      }

}

Toujours à la recherche de la sortie via une seule fonction. Mettra à jour une fois terminé.

4
Jaiprakash

En utilisant l'approche de Grouper par plusieurs noms de champs dans Java 8 avec une classe de clé personnalisée, ma suggestion est la suivante:

    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)
            )));

La DepSubject définit equals et hashCode. De cette manière, la classe d'origine ne doit pas être modifiée et, si plusieurs critères de regroupement sont nécessaires, plusieurs classes peuvent être utilisées. Malheureusement, cela peut être très bavard en Java, car vous avez besoin d’une classe avec des égaux, hashCode, (getters, setters). En fait, à mon avis, les accesseurs et les setters pourraient également être omis, si la classe n’est utilisée que dans un seul endroit pour le regroupement.

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
    }

Il est également possible de rassembler les résultats dans une liste. De cette façon, les classes personnalisées DepSubject et Grades sont uniquement utilisées pour des opérations intermédiaires:

    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())
            ));

Vous pouvez également extraire la logique de groupingBy dans une fonction:

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())
            ));
}

De cette façon, vous pouvez grouper vos personnes de cette façon:

    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));

Si vous souhaitez grouper votre objet uniquement par sujet, vous pouvez simplement faire:

    List<Person> list2 = groupBy(persons,
            Person::getSubject,
            (subject,grades) -> new Person(null,subject, null, grades.m1, grades.m2, grades.m3));
1
user140547