JavaRush /Курси /JAVA 25 SELF /Операції union, intersection, difference

Операції union, intersection, difference

JAVA 25 SELF
Рівень 32 , Лекція 2
Відкрита

1. Операція union (обʼєднання множин)

У Java для представлення множин використовується інтерфейс Set<T>. На відміну від списків (List), множини гарантують унікальність елементів і зазвичай не зберігають порядок (якщо не застосовувати спеціальні реалізації, як-от LinkedHashSet). Найпоширеніші реалізації — HashSet і TreeSet. Їхнє основне завдання — швидко визначати наявність елемента та забезпечувати відсутність дублікатів.

Коли потрібні множини?

  • Коли важлива унікальність: наприклад, список усіх унікальних відвідувачів сайту.
  • Коли потрібно швидко перевіряти наявність елемента: метод contains у HashSet зазвичай працює за сталий час.
  • Коли потрібно виконувати типові операції над множинами: обʼєднання, перетин і різницю.

Union — це обʼєднання двох або більше множин: результат містить усі елементи з обох вихідних множин (без повторів).

Приклад на практиці

Припустімо, у нас є дві множини студентів, які відвідують гуртки «Робототехніка» та «Програмування»:

Set<String> robotics = Set.of("Аня", "Борис", "Віка");
Set<String> programming = Set.of("Віка", "Гліб", "Даша");

Нам потрібно отримати множину всіх студентів, які ходять принаймні до одного гуртка.

Рішення через Stream API

Найпряміший спосіб — обʼєднати обидва потоки та зібрати їх у Set:

Set<String> all = Stream.concat(
        robotics.stream(),
        programming.stream()
    ).collect(Collectors.toSet());

System.out.println(all); // [Аня, Борис, Віка, Гліб, Даша]

Пояснення:

  • Stream.concat обʼєднує два потоки.
  • collect(Collectors.toSet()) збирає елементи у множину (автоматично прибираючи дублікати).

Альтернатива: понад дві множини

Якщо у нас три і більше гуртки, використовуйте Stream.of і flatMap:

Set<String> math = Set.of("Женя", "Віка", "Борис");

Set<String> all = Stream.of(robotics, programming, math)
    .flatMap(Set::stream)
    .collect(Collectors.toSet());

System.out.println(all); // [Аня, Борис, Віка, Гліб, Даша, Женя]

Чому саме Set?
Тому що Set автоматично прибирає дублікати. Якщо зібрати в List, одні й ті самі імена повторяться.

2. Операція intersection (перетин множин)

Intersection — це елементи, які є одночасно в обох множинах.

Приклад на практиці

Знайти студентів, які ходять і до «Робототехніки», і до «Програмування»:

Set<String> robotics = Set.of("Аня", "Борис", "Віка");
Set<String> programming = Set.of("Віка", "Гліб", "Даша");

Рішення через Stream API:

Set<String> both = robotics.stream()
    .filter(programming::contains)
    .collect(Collectors.toSet());

System.out.println(both); // [Віка]

Пояснення:
Ми перебираємо всіх учасників «Робототехніки» і фільтруємо лише тих, хто є в «Програмуванні». Підсумок — множина з іменами, які є в обох гуртках.

Альтернативний спосіб (без Stream API)

Можна використати вбудований метод retainAll (він змінює поточну множину):

Set<String> intersection = new HashSet<>(robotics);
intersection.retainAll(programming);
System.out.println(intersection); // [Віка]

У межах цієї теми зосередимося на потоках.

3. Операція difference (різниця множин)

Difference — це елементи першої множини, яких немає у другій.

Приклад на практиці

Знайти студентів, які ходять лише до «Робототехніки», але не до «Програмування»:

Set<String> robotics = Set.of("Аня", "Борис", "Віка");
Set<String> programming = Set.of("Віка", "Гліб", "Даша");

Рішення через Stream API:

Set<String> onlyRobotics = robotics.stream()
    .filter(name -> !programming.contains(name))
    .collect(Collectors.toSet());

System.out.println(onlyRobotics); // [Аня, Борис]

Пояснення:
Ми фільтруємо учасників «Робототехніки», залишаючи тільки тих, кого немає в «Програмуванні».

Альтернативний спосіб (без Stream API)

Set<String> difference = new HashSet<>(robotics);
difference.removeAll(programming);
System.out.println(difference); // [Аня, Борис]

4. Практичні завдання: обробка списків користувачів

Завдання 1: Знайти студентів, які ходять лише до одного гуртка

Потрібно дізнатися, хто ходить лише до «Робототехніки» або лише до «Програмування», але не до обох одразу. Це симетрична різниця (xor для множин):

Set<String> onlyOne = Stream.concat(
        robotics.stream().filter(name -> !programming.contains(name)),
        programming.stream().filter(name -> !robotics.contains(name))
    ).collect(Collectors.toSet());

System.out.println(onlyOne); // [Аня, Борис, Гліб, Даша]

Завдання 2: Список усіх унікальних студентів із кількох гуртків

Set<String> all = Stream.of(robotics, programming, math)
    .flatMap(Set::stream)
    .collect(Collectors.toSet());

System.out.println(all); // [Аня, Борис, Віка, Гліб, Даша, Женя]

Завдання 3: Знайти студентів, які не ходять до жодного гуртка

Припустімо, у нас є список усіх учнів класу:

Set<String> allStudents = Set.of("Аня", "Борис", "Віка", "Гліб", "Даша", "Женя", "Ігор", "Катя");

Потрібно дізнатися, хто не ходить до жодного гуртка:

Set<String> attendees = Stream.of(robotics, programming, math)
    .flatMap(Set::stream)
    .collect(Collectors.toSet());

Set<String> notInAny = allStudents.stream()
    .filter(name -> !attendees.contains(name))
    .collect(Collectors.toSet());

System.out.println(notInAny); // [Ігор, Катя]

5. Важливі зауваження: equals, hashCode та ефективність

Чому важливо правильно реалізовувати equals і hashCode?

Усі операції з множинами (Set) залежать від коректності методів equals і hashCode. Якщо ви зберігаєте обʼєкти власного класу (наприклад, Student), обовʼязково перевизначайте ці методи, інакше порівняння працюватимуть некоректно.

Приклад:

class Student {
    String name;
    int age;

    // Не забудьте перевизначити equals і hashCode!
}

Якщо цього не зробити, двоє студентів з однаковими іменами та віком вважатимуться різними обʼєктами для Set.

Чому краще використовувати Set, а не List?

  • Операція contains у Set працює швидко (зазвичай за сталий час).
  • У List пошук елемента відбувається за лінійний час, що може бути критично для великих колекцій.
  • Для операцій над множинами (union, intersection, difference) Set значно ефективніший і логічніший.

6. Типові помилки під час роботи з операціями над множинами

Помилка № 1: Використання List замість Set для операцій над множинами. Якщо ви збираєте елементи в List, дублікати не видаляються, а операція contains працює повільно. Для union/intersection/difference використовуйте Set.

Помилка № 2: Не реалізовано equals/hashCode для обʼєктів. Якщо ви зберігаєте в Set обʼєкти власного класу, але не перевизначили методи equals і hashCode, то перетин і різниця працюватимуть «дивно» — обʼєкти, які за змістом однакові, не вважатимуться рівними.

Помилка № 3: Модифікація колекції під час потоку. Якщо ви безпосередньо в потоці намагаєтеся змінювати вихідний Set (наприклад, додавати чи видаляти елементи), отримаєте ConcurrentModificationException. Завжди працюйте з новою множиною.

Помилка № 4: Неочевидна втрата порядку. HashSet не гарантує порядок елементів. Якщо порядок важливий — використовуйте LinkedHashSet або TreeSet.

Помилка № 5: Використання Stream.concat для понад двох колекцій. Stream.concat обʼєднує лише два потоки. Для більшої кількості використовуйте Stream.of(...) і flatMap.

Помилка № 6: Проблеми з null. Множини не люблять значення null, особливо якщо ви використовуєте Set.of(...) — він не допускає null. Для роботи із null використовуйте інші реалізації або фільтруйте значення заздалегідь.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ