CodeGym /Kurslar /JAVA 25 SELF /Mürəkkəb aqreqasiya: iç-içə qruplaşdırmalar

Mürəkkəb aqreqasiya: iç-içə qruplaşdırmalar

JAVA 25 SELF
Səviyyə , Dərs
Mövcuddur

1. İç-içə groupingBy: sintaksis və işləmə prinsipi

Həyatda məlumatları yalnız bir əlamətə görə qruplaşdırmaq nadir hallarda kifayət edir. Məsələn, sizdə şirkət əməkdaşlarının siyahısı varsa, çox vaxt təkcə hər şöbədə neçə nəfər olduğunu deyil, həm də hər şöbədə hər vəzifə üzrə neçə nəfər olduğunu bilmək istənilir. Və ya tələbələr bazanız varsa — hər kursda hər ixtisas üzrə neçə tələbə var.

İç-içə qruplaşdırmalar belə «xəritə içində xəritə» qurmağa imkan verir — «şöbə → vəzifə → əməkdaşların siyahısı» və ya «kurs → ixtisas → tələbələrin siyahısı» strukturu.

Stream API olmasa, bu cür tapşırıqlar bir neçə iç-içə dövrlə və əl ilə Map daxilində Map qurmaqla həll edilərdi. Axınlar və kollektorlarla bunu bir-iki sətrə sığdırmaq olur.

İç-içə qruplaşdırma odur ki, Collectors.groupingBy metodunun ikinci arqumenti kimi başqa bir kollektoru, məsələn, daha bir groupingBy-ı verirsiniz. Nəticədə, hər açar üçün dəyəri başqa bir xəritə olan bir xəritə alınır.

Ümumi şablon

Map<Klyuch1, Map<Klyuch2, List<T>>> result = 
    stream.collect(Collectors.groupingBy(
        obyekt -> klyuch1,
        Collectors.groupingBy(obyekt -> klyuch2)
    ));

Nümunə: şöbə və vəzifə üzrə əməkdaşlar

Tutaq ki, belə bir sinfimiz var:

class Employee {
    private String name;
    private String department;
    private String position;
    private int salary;
    // ... konstruktorlar, getter-lər, toString
}

Əməkdaşların siyahısı:

List<Employee> employees = List.of(
    new Employee("Ivan", "IT", "Tərtibatçı", 120_000),
    new Employee("Mariya", "IT", "Testçi", 90_000),
    new Employee("Pyotr", "HR", "Menecer", 80_000),
    new Employee("Olga", "IT", "Tərtibatçı", 130_000),
    new Employee("Svetlana", "HR", "Rekruter", 70_000)
);

Şöbə və vəzifə üzrə qruplaşdırırıq:

Map<String, Map<String, List<Employee>>> grouped = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.groupingBy(Employee::getPosition)
    ));

Nə əldə etdik?
Xəritə: açar — şöbə, dəyər — xəritə (açar — vəzifə, dəyər — əməkdaşların siyahısı).

Strukturun vizuallaşdırılması

IT:
  Tərtibatçı: [Ivan, Olga]
  Testçi: [Mariya]
HR:
  Menecer: [Pyotr]
  Rekruter: [Svetlana]

2. Aqreqasiya ilə qruplaşdırma: groupingBy və aqreqatorların birləşdirilməsi

İç-içə qruplaşdırmalar təkcə siyahı toplamaqla məhdudlaşmır! Hər alt qrup daxilində dərhal aqreqasiya etmək olar.

Nümunə: şöbə üzrə maksimal maaş

Map<String, Optional<Employee>> maxSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
    ));

Burada hər şöbə üçün maksimal maaşa malik əməkdaşı alırıq (nəticə Optional ilə qablaşdırılıb, çünki şöbə boş ola bilər).

İç-içə qruplaşdırma + aqreqasiya

Tutaq ki, hər şöbədə hər vəzifə üzrə maksimal maaşı bilmək istəyirik:

Map<String, Map<String, Optional<Employee>>> maxSalaryByDeptAndPos = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.groupingBy(
            Employee::getPosition,
            Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
        )
    ));

Bu nə deməkdir?

  • Hər şöbə üçün — vəzifələr xəritəsi.
  • Hər vəzifə üçün — maksimal maaşlı əməkdaş (və ya heç kim yoxdursa, boş Optional).

3. Transformasiya ilə qruplaşdırma: mapping groupingBy daxilində

Bəzən obyektləri sadəcə qruplaşdırmaq deyil, qruplar daxilində yalnız müəyyən sahələri almaq lazımdır.

Nümunə: şöbələrə görə əməkdaşların adları

Map<String, List<String>> namesByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.mapping(Employee::getName, Collectors.toList())
    ));

Nəticə:

IT: [Ivan, Mariya, Olga]
HR: [Pyotr, Svetlana]

İç-içə mapping

mapping-i iç-içə groupingBy ilə birləşdirmək olar:

Map<String, Map<String, List<String>>> namesByDeptAndPos = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.groupingBy(
            Employee::getPosition,
            Collectors.mapping(Employee::getName, Collectors.toList())
        )
    ));

Nəticə:

IT:
  Tərtibatçı: [Ivan, Olga]
  Testçi: [Mariya]
HR:
  Menecer: [Pyotr]
  Rekruter: [Svetlana]

4. Qruplaşdırma + ədədi məlumatların aqreqasiyası

Çox vaxt təkcə qruplaşdırmaq deyil, həm də qruplar üzrə cəm, orta və ya say hesablamaq lazım olur.

Nümunə: şöbə üzrə orta maaş

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingInt(Employee::getSalary)
    ));

İç-içə aqreqasiya

Hər şöbədə vəzifə üzrə orta maaş:

Map<String, Map<String, Double>> avgSalaryByDeptAndPos = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.groupingBy(
            Employee::getPosition,
            Collectors.averagingInt(Employee::getSalary)
        )
    ));

5. partitioningBy + aqreqasiya

Bəzən kolleksiyanı məntiqi (boolean) əlamətə görə iki qrupa bölmək və daxilində də aqreqasiya etmək rahatdır.

Nümunə: hər şöbədə maaşı 100_000-dən yüksək olan neçə əməkdaş var

Map<String, Map<Boolean, Long>> countByDeptAndSalary = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.partitioningBy(
            e -> e.getSalary() > 100_000,
            Collectors.counting()
        )
    ));

Nəticə:
Hər şöbə üçün — xəritə: true/false → əməkdaşların sayı.

6. Praktiki məsələlər: iç-içə qruplaşdırmaların tətbiqi

Məsələ 1. Kurs və ixtisas üzrə tələbələr

class Student {
    private String name;
    private int course;
    private String speciality;
    private double grade;
    // ... getter-lər, konstruktor
}

List<Student> students = ... // tutaq ki, artıq var

Map<Integer, Map<String, List<Student>>> byCourseAndSpec = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.groupingBy(Student::getSpeciality)
    ));

Məsələ 2. Kurs üzrə orta bal

Map<Integer, Double> avgGradeByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.averagingDouble(Student::getGrade)
    ));

Məsələ 3. Qruplar üzrə yalnız adlar

Map<Integer, Map<String, List<String>>> namesByCourseAndSpec = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.groupingBy(
            Student::getSpeciality,
            Collectors.mapping(Student::getName, Collectors.toList())
        )
    ));

7. Faydalı nüanslar

İç-içə Map-lərdən məlumatları necə oxumaq və çıxarmaq olar

İç-içə xəritələrlə işləmək əvvəlcə alışılmamış ola bilər. Budur əsas nümunə:

for (var deptEntry : grouped.entrySet()) {
    String dept = deptEntry.getKey();
    Map<String, List<Employee>> byPosition = deptEntry.getValue();
    System.out.println("Şöbə: " + dept);
    for (var posEntry : byPosition.entrySet()) {
        String pos = posEntry.getKey();
        List<Employee> emps = posEntry.getValue();
        System.out.println("  Vəzifə: " + pos + " -> " + emps);
    }
}

İç-içə qruplaşdırmanın sxemi

Map<Şöbə, Map<Vəzifə, List<Employee>>>
      │
      ├── "IT"
      │      ├── "Tərtibatçı" → [Ivan, Olga]
      │      └── "Testçi"     → [Mariya]
      └── "HR"
             ├── "Menecer"   → [Pyotr]
             └── "Rekruter"  → [Svetlana]

Cədvəl: müxtəlif kombinasiya nəticələri

Kollektor Nəticə
groupingBy(Employee::getDepartment)
Map<String, List<Employee>>
groupingBy(Employee::getDepartment, averagingInt(...))
Map<String, Double>
groupingBy(Employee::getDepartment, groupingBy(...))
Map<String, Map<String, List<Employee>>>
groupingBy(..., mapping(..., toList()))
Map<..., List<...>>
groupingBy(..., groupingBy(..., mapping(..., toList())))
Map<..., Map<..., List<...>>>

8. İç-içə qruplaşdırmalarla işləyərkən tipik səhvlər

Səhv №1: İç-içə Map-lərin strukturunu yanlış anlamaq.
İç-içə qruplaşdırmalardan sonra hər bir xəritənin dəyərində nə olduğunu qarışdırmaq asandır. Həmişə nəticənin siqnatürasına baxın — IDE tipi göstərəcək. Əmin deyilsinizsə, nəticəni System.out.println(grouped) ilə ekrana çıxarın və ya sazlayıcıdan istifadə edin.

Səhv №2: Məlumat çıxararkən NullPointerException.
Açar yoxdursa (məsələn, şöbədə müəyyən vəzifə üzrə əməkdaş yoxdur), Map.get(key) null qaytaracaq. Açarın mövcudluğunu containsKey ilə yoxlayın və ya Java 8-dən etibarən Map.getOrDefault-dan, Java 9-dan isə Map.ofNullableOptional metodlarından istifadə edə bilərsiniz.

Səhv №3: Həddindən artıq mürəkkəb iç-içəlilik.
Əgər qruplaşdırma çox dərinləşirsə (3–4 səviyyə), bəlkə də məlumat strukturuna yenidən baxmaq və ya tapşırığı daha kiçik mərhələlərə bölmək lazımdır.

Səhv №4: Aqreqasiyanın yanlış səviyyəsi.
Bəzən aqreqasiya edən kollektoru (averagingInt, counting) lazım olan groupingBy-ın içinə deyil, kənarına yerləşdirirlər — və gözlənilməz nəticə alırlar. Həmişə mötərizələri diqqətlə yerləşdirin!

Səhv №5: collect zamanı elementləri dəyişməyə cəhd.
Qruplaşdırma prosesində ilkin kolleksiyaları və ya obyektləri dəyişməyin — bu, çətin izlənən xətalara səbəb ola bilər.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION