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); // [Вика]
Но в рамках темы Stream API акцентируем внимание на стримах.
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 используйте другие реализации или фильтруйте значения заранее.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ