1. Giriş
Real həyatda demək olar ki, həmişə məlumatları qruplaşdırmaq lazım olur. Məsələn, tələbələri kurslara görə ayırmaq, malları kateqoriyalara bölmək, yetkinləri uşaqlardan ayırmaq və sair.
Stream API olmadan belə məsələlər əl ilə həll olunurdu: kolleksiyanı dövr etmək, əlaməti yoxlamaq, Map daxilində lazımi siyahıya əlavə etmək. Budur tipik “old-school” kod:
Map<String, List<Employee>> byDepartment = new HashMap<>();
for (Employee employee : employees) {
String dept = employee.getDepartment();
byDepartment.computeIfAbsent(dept, k -> new ArrayList<>()).add(employee);
}
İşləyir, amma elə görünür ki, sanki makaronları bankalara əl ilə çeşidləyirsiniz. Bəs bir neçə əlamətə görə qruplaşdırmaq lazımdırsa? Yaxud qruplar üzrə cəmləri də hesablamaq? Kod şişir və oxunmaz olur.
Stream API və xüsusi kollektorlar (groupingBy, partitioningBy) bunu bir-iki sətirdə etməyə imkan verir — və bu zaman əsl Java-sehrbazı kimi görünürsünüz.
2. Kollektor groupingBy: əlamətə görə qruplaşdırma
Əsas ideya
groupingBy — axın elementlərini Map-ə çevirən kollektorudur; burada açar — əlamət funksiyasının nəticəsi, dəyər isə həmin əlamətə uyğun elementlərin siyahısıdır.
İmza:
Collectors.groupingBy(Function<T, K>)
- T — axındakı elementin tipi,
- K — funksiyanın qaytardığı açarın (qrupun) tipi.
Sadə nümunə: əməkdaşları şöbəyə görə qruplaşdırma
Tutaq ki, belə bir sinfimiz var:
public class Employee {
private final String name;
private final String department;
// ... konstruktor və getter-lər
public Employee(String name, String department) {
this.name = name;
this.department = department;
}
public String getName() { return name; }
public String getDepartment() { return department; }
}
Və əməkdaşların kolleksiyası:
List<Employee> employees = List.of(
new Employee("Alisa", "IT"),
new Employee("Bob", "HR"),
new Employee("Klara", "IT"),
new Employee("Denis", "Finance"),
new Employee("Eva", "HR")
);
Şöbəyə görə qruplaşdıraq:
Map<String, List<Employee>> byDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
Nə almış olduq?
- Açar: şöbənin adı (String).
- Dəyər: həmin şöbənin əməkdaşlarının siyahısı (List<Employee>).
Nəticəni çap edirik:
byDepartment.forEach((dept, emps) -> {
System.out.println(dept + ": " +
emps.stream().map(Employee::getName).toList());
});
Çıxış:
IT: [Alisa, Klara]
HR: [Bob, Eva]
Finance: [Denis]
Bu, pərdə arxasında necə işləyir?
Əvvəlcə axının hər bir elementi üçün açar hesablanır (məsələn, getDepartment()). Əgər belə açar artıq Map-də varsa, element uyğun siyahıya əlavə edilir. Açar yoxdursa — yeni siyahı yaradılır.
Bənzətmə
Təsəvvür edin ki, məktubları qovluqlara görə çeşidləyirsiniz: hər məktub üçün baxırsınız ki, lazım olan adlı qovluq artıq varmı; yoxdursa — yenisini yaradırsınız və məktubu ora qoyursunuz.
3. Kollektor partitioningBy: iki yerə bölmə
Bəzən “dəyərə görə qruplaşdırmaq”dan çox, sadəcə məntiqi əlamətə görə (true/false) kolleksiyanı iki hissəyə bölmək lazımdır. Məsələn, əməkdaşları müəyyən həddən yuxarı və aşağı maaş alanlara, tələbələri — “keçdi/keçmədi” olaraq.
Bunun üçün xüsusi kollektor var — partitioningBy.
İmza:
Collectors.partitioningBy(Predicate<T>)
Predicate — boolean dəyər qaytaran funksiyadır.
Nümunə: əməkdaşları maaş səviyyəsinə görə bölmək
Tutaq ki, belədir:
public class Employee {
private final String name;
private final int salary;
// ... konstruktor və getter-lər
public Employee(String name, int salary) {
this.name = name;
this.salary = salary;
}
public String getName() { return name; }
public int getSalary() { return salary; }
}
Və siyahı:
List<Employee> employees = List.of(
new Employee("Alisa", 120_000),
new Employee("Bob", 80_000),
new Employee("Klara", 150_000),
new Employee("Denis", 95_000)
);
“Varlılar” və “təvazökarlar”a bölək:
Map<Boolean, List<Employee>> partitioned = employees.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 100_000));
- true — maaşı 100_000-dən çox olan əməkdaşlar.
- false — qalanlar.
Çap edək:
System.out.println("Varlılar: " +
partitioned.get(true).stream().map(Employee::getName).toList());
System.out.println("Təvazökarlar: " +
partitioned.get(false).stream().map(Employee::getName).toList());
Nəticə:
Varlılar: [Alisa, Klara]
Təvazökarlar: [Bob, Denis]
partitioningBy nə vaxt, groupingBy nə vaxt istifadə etməli?
- Əgər iki qrupdan çoxdursa — groupingBy istifadə edin.
- Əgər yalnız boolean əlamətinə görə iki qrupdursa — partitioningBy istifadə edin: bu daha sürətli və anlaşıqlıdır.
4. İç-içə qruplaşdırmalar: bir neçə əlamətə görə qruplaşdırma
Bəzən təkcə bir əlamətə görə yox, həm də “bir qruplaşdırmanı digərinin içinə” yerləşdirmək istəyirsiniz. Məsələn, əməkdaşları əvvəlcə şöbəyə, şöbənin içində isə vəzifəyə görə qruplaşdırmaq.
Nümunə:
Tutaq ki, bizim Employee sinfimizdə əlavə olaraq position sahəsi var:
public class Employee {
private final String name;
private final String department;
private final String position;
// ... konstruktor və getter-lər
}
Daxili qruplaşdırma:
Map<String, Map<String, List<Employee>>> byDeptAndPosition = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.groupingBy(Employee::getPosition)));
- Xarici açar — şöbə.
- Daxili açar — vəzifə.
- Dəyər — əməkdaşların siyahısı.
IT şöbəsində “Developer” vəzifəsində olan əməkdaşlara necə müraciət etməli?
List<Employee> itDevs = byDeptAndPosition
.getOrDefault("IT", Map.of())
.getOrDefault("Developer", List.of());
Vizual sxem
Map<Department, Map<Position, List<Employee>>>
└─ "IT"
├─ "Developer" -> [Alisa, Klara]
└─ "QA" -> [Boris]
└─ "HR"
└─ "Recruiter" -> [Denis]
5. Praktik qruplaşdırma nümunələri
Nümunə 1: Sətirləri uzunluğa görə qruplaşdırma
List<String> words = List.of("cat", "dog", "elephant", "bee", "ant", "dolphin");
Map<Integer, List<String>> byLength = words.stream()
.collect(Collectors.groupingBy(String::length));
byLength.forEach((len, ws) -> System.out.println(len + ": " + ws));
Çıxış:
3: [cat, dog, bee, ant]
8: [elephant, dolphin]
Nümunə 2: Rəqəmləri cüt/tək üzrə qruplaşdırma
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
Map<String, List<Integer>> byParity = numbers.stream()
.collect(Collectors.groupingBy(n -> n % 2 == 0 ? "even" : "odd"));
System.out.println(byParity);
// {odd=[1, 3, 5], even=[2, 4, 6]}
Nümunə 3: Sətirlər üçün partitioningBy istifadə edilməsi
Sətirləri “A” ilə başlayanlar və qalanlar olaraq bölək:
List<String> names = List.of("Alice", "Bob", "Anna", "Charlie");
Map<Boolean, List<String>> byA = names.stream()
.collect(Collectors.partitioningBy(s -> s.startsWith("A")));
System.out.println("A-names: " + byA.get(true)); // [Alice, Anna]
System.out.println("Other: " + byA.get(false)); // [Bob, Charlie]
5. Faydalı nüanslar
Qruplaşdırmanın nəticəsindən necə istifadə etməli
Çox vaxt qruplaşdırmadan sonra sadəcə Map əldə etmək yox, onu nəsə emal etmək istəyirik:
- Bütün qrupları keçmək və məlumatı çıxarmaq.
- Ən çox element olan qrupu tapmaq.
- Hər qrup üçün, məsələn, cəm və ya orta hesabla — bu barədə daha ətraflı növbəti mühazirədə.
Nümunə: hər şöbədəki əməkdaşların sayını çap etmək
byDepartment.forEach((dept, emps) ->
System.out.println(dept + ": " + emps.size() + " əməkdaş"));
Əl ilə reallaşdırma ilə müqayisə
Möhkəmləndirmək üçün: şöbəyə görə qruplaşdırma “köhnə üsulla” belə görünərdi:
Map<String, List<Employee>> byDepartment = new HashMap<>();
for (Employee e : employees) {
String dept = e.getDepartment();
byDepartment.computeIfAbsent(dept, k -> new ArrayList<>()).add(e);
}
Və Stream API ilə:
Map<String, List<Employee>> byDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
Nəticə: daha az kod, daha az səhv, oxunaqlıq daha yüksəkdir.
Lifehack-lər
Əgər List-ə yox, məsələn, Set-ə qruplaşdırmaq lazımdırsa — ikinci parametr kimi Collectors.toSet() istifadə edin:
Collectors.groupingBy(Employee::getDepartment, Collectors.toSet())
Birbaşa aqreqasiya da etmək olar: məsələn, hər şöbədə əməkdaşların sayını almaq:
Collectors.groupingBy(Employee::getDepartment, Collectors.counting())
Amma bunun haqqında — növbəti mühazirədə!
partitioningBy-dan sonra həmişə iki açar olacaq: true və false. Hətta qruplardan biri boş olsa belə.
Daxili qruplaşdırmalardan sonra struktur “ağac” olur: Map içində Map və s.
6. Qruplaşdırma və partitioning zamanı tipik səhvlər
Səhv № 1: Nəticə tipinin yanlışdır. Yeni başlayanlar tez-tez groupingBy nəticəsinin sadəcə List<T> olduğunu, yoxsa Map<K, List<T>> olmadığını gözləyirlər. Nəticədə siyahı metodlarını çağırmağa çalışır və kompilasiya xətası alırlar. Unutmayın: qruplaşdırma həmişə Map-dir!
Səhv № 2: Mövcud olmayan qrupa müraciət edərkən NullPointerException. Əgər mövcud olmayan açar üzrə siyahı almağa çalışsanız — null alacaqsınız. getOrDefault(key, List.of()) istifadə edin və ya açarın mövcudluğunu containsKey ilə yoxlayın.
Səhv № 3: Bir neçə qrup olan tapşırıqlarda partitioningBy istifadə etmək. partitioningBy — yalnız iki qrup (true/false) üçündür. Qruplar çoxdursa — groupingBy istifadə edin.
Səhv № 4: Stream daxilində kolleksiyaları dəyişmək. Mənbə kolleksiyaları və ya Map-i stream daxilində dəyişməyə çalışmayın — bu, gözlənilməz xətalara səbəb olacaq. Bütün emalı Stream API və kollektorlar vasitəsilə edin.
Səhv № 5: İç-içə qruplaşdırmaların strukturunun aydın olmaması. İç-içə groupingBy-dan sonra nəticə — Map içində Map-dir. Məlumatı düzgün çıxardın (məsələn, getOrDefault vasitəsilə), əks halda ClassCastException ala bilərsiniz.
GO TO FULL VERSION