web-dev-qa-db-ja.com

Java内のオブジェクトの集約リスト

Javaに以下の集計を実行するための集計関数がありますか?

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

リストには以下のデータが含まれています。

名前|件名|部門| Mark1 | Mark2 | Mark3 
 -------- | ----------- | -------- --- | ------- | ------- | ----- 
クラーク|英語| DEP1 | 7 | 8 | 6 
ミシェル|英語| DEP1 | 6 | 4 | 7 
デイブ|数学| DEP2 | 3 | 5 | 6 
マリオ|数学| DEP1 | 9 | 7 | 8 

集計基準はSubject&Depです。結果のオブジェクトは

件名|部門| Mark1 | Mark2 | Mark3 
 ----------- | ----------- | ------- | ------- | ----- 
英語| DEP1 | 13 | 12 | 13 
数学| DEP2 | 3 | 5 | 6 
数学| 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の集計関数または機能はありますか?

13
Swadeesh

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

これにより、最初に要素が主題ごとにグループ化され、次に部門ごとにグループ化され、マークを合計することで2番目のマップの結果の値が減少します。

あなたの例であなたが持っている人のリストでそれを実行すると、あなたは出力として得られます:

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

もちろん、これらのソリューションの「美しさ」について自問することもできます。意図をより明確にするために、カスタムコレクターまたはカスタムクラスを導入したい場合もあります。

3
Alexis C.

reduction を使用できます。 mark1を集計するサンプルは以下のとおりです。

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

      }

}

単一の関数を介して出力を生成することをまだ検討しています。完了すると更新されます。

4
Jaiprakash

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

DepSubjectequalshashCodeを定義します。これにより、元のクラスを変更する必要がなくなり、複数のグループ化基準が必要な場合は、複数のクラスを使用できます。残念ながら、equals、hashCode(getter、setter)を持つクラスが必要なため、Javaではこれは非常に冗長になる可能性があります。実際、私の意見では、クラスがグループ化のために1つの場所でのみ使用されている場合、ゲッターとセッターも省略できると思います。

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
    }

結果をリストに収集することも可能です。このように、カスタムクラスDepSubjectGradesは中間操作にのみ使用されます。

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