CodeGym /Kurslar /JAVA 25 SELF /groupingBy və partitioningBy metodları (Collectors)

groupingBy və partitioningBy metodları (Collectors)

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

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

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: truefalse. 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.

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