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 використовуйте інші реалізації або фільтруйте значення заздалегідь.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ