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ə |
|---|---|
|
|
|
|
|
|
|
|
|
|
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.ofNullable və Optional 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.
GO TO FULL VERSION