1. Вступ
Почнімо з життєвого прикладу. Уявіть, що ви організовуєте вечірку та складаєте список гостей. Ви розсилаєте запрошення, а потім зʼясовується, що одна й та сама людина опинилася у списку двічі (або навіть тричі — ну, він любить вечірки!). Якщо ви використовуєте звичайний список (List), такі дублікати легко можуть зʼявитися. Але якби у вас була колекція, яка сама не дозволяла б додати одного й того самого гостя двічі — життя стало б простішим.
Ось тут у пригоді стає колекція Set — множина.
Set — це колекція, що зберігає лише унікальні елементи. Якщо спробувати додати вже наявний елемент, його просто не буде додано (і ніхто не образиться).
Інтерфейс Set: основні властивості
У Java Set — це інтерфейс, який визначає поведінку колекції без дублікатів. Він розширює інтерфейс Collection, тож підтримує такі операції, як додавання (add), видалення (remove), перевірка наявності елемента (contains) та перебирання елементів.
Ключові особливості:
- У Set не може бути двох однакових елементів.
- Елементи можуть зберігатися у довільному порядку — залежить від конкретної реалізації.
- Немає індексів: не можна звернутися до елемента за номером, як у списку.
Синтаксис оголошення
Set<String> guests = new HashSet<>();
2. HashSet: швидко, просто, без порядку
HashSet — найпопулярніша реалізація інтерфейсу Set. Вона ґрунтується на хеш-таблиці (як і HashMap, тільки без пари «ключ-значення», а просто з унікальними значеннями). Головна перевага — швидкість операцій додавання, видалення та пошуку.
Як працює HashSet?
Уявіть собі ящик, у який ви складаєте речі. Щоб швидко зрозуміти, чи є в ньому вже щось подібне, кожна річ отримує свій «номер» — хеш-код. Коли ви додаєте елемент до HashSet, спершу обчислюється цей хеш-код. Якщо такого ще не було, елемент спокійно кладеться в колекцію. Якщо ж такий хеш уже є, то додатково перевіряється рівність через equals(). І лише якщо обʼєкти справді збігаються, новий елемент не буде додано.
Тобто HashSet автоматично дбає про унікальність: два однакові обʼєкти в ньому не зʼявляться.
Цікавий момент: якщо ви працюєте з власними класами й хочете зберігати їх у HashSet, доведеться перевизначити методи equals() і hashCode(). Без цього колекція може поводитися непередбачувано: однакові обʼєкти вважатимуться різними.
Основні методи HashSet
Set<String> guests = new HashSet<>();
guests.add("Іван");
guests.add("Марія");
guests.add("Петро");
guests.add("Іван"); // Дублікат! Не буде додано.
System.out.println(guests); // [Іван, Марія, Петро] — порядок може бути будь-яким
guests.remove("Петро"); // Видаляємо елемент
System.out.println(guests.contains("Марія")); // true
System.out.println(guests.size()); // 2
Спробуймо це в коді
Припустімо, у нашому застосунку ми хочемо зберігати унікальні назви завдань, щоб не зʼявлялися дублікати назв:
import java.util.HashSet;
import java.util.Set;
public class UniqueTasksDemo {
public static void main(String[] args) {
Set<String> tasks = new HashSet<>();
tasks.add("Зробити домашнє завдання з Java");
tasks.add("Погладити кота");
tasks.add("Зробити домашнє завдання з Java"); // Дублікат!
System.out.println("Список завдань:");
for (String task : tasks) {
System.out.println("- " + task);
}
// У списку буде лише два завдання — дублікат не буде додано
}
}
3. TreeSet: порядок важливий!
Іноді нам потрібна не лише унікальність, а й відсортований набір елементів. Наприклад, хочеться бачити імена гостей за абеткою, а не у випадковому порядку. Для цього є TreeSet.
TreeSet — це реалізація інтерфейсу Set, яка зберігає елементи у відсортованому порядку (за зростанням). Вона побудована на структурі «червоно-чорне дерево».
Приклад використання TreeSet
import java.util.Set;
import java.util.TreeSet;
public class SortedGuestsDemo {
public static void main(String[] args) {
Set<String> guests = new TreeSet<>();
guests.add("Володимир");
guests.add("Олексій");
guests.add("Катерина");
guests.add("Олексій"); // Дублікат!
System.out.println("Гості (за абеткою):");
for (String guest : guests) {
System.out.println("- " + guest);
}
// Виведення:
// - Володимир
// - Катерина
// - Олексій
}
}
Зверніть увагу: якщо ви додаєте дублікат, він не зʼявиться в множині. Усе, як і має бути!
Коли використовувати TreeSet?
- Коли потрібен відсортований набір унікальних елементів.
- Коли важливий швидкий пошук, але швидкість додавання не критична (працює трохи повільніше, ніж HashSet).
- Якщо елементи — ваші власні класи, вони мають бути «порівнюваними» (реалізовувати інтерфейс Comparable) або ви маєте надати свій Comparator.
4. Корисні нюанси
HashSet vs TreeSet: що обрати?
| Критерій | HashSet | TreeSet |
|---|---|---|
| Порядок зберігання | Не гарантується | Відсортовано за зростанням |
| Швидкість операцій | Швидше (O(1)) | Повільніше (O(log n)) |
| Вимоги до типу | Будь-який тип (достатньо equals()/hashCode()) | Comparable або Comparator |
| Типові сценарії | Коли потрібен швидкий доступ до унікальних елементів | Коли важливе впорядковане виведення/обхід |
Особливості роботи з Set
- Немає індексів. На відміну від List, у Set немає методу get(int index). Якщо потрібен доступ за індексом — використовуйте List.
- Немає дублікатів. Якщо ви намагаєтеся додати елемент, який уже є, він не додасться. Метод add поверне false.
- Порядок не гарантується (окрім TreeSet). У HashSet порядок елементів може відрізнятися під час кожного запуску. Якщо потрібен порядок додавання — використовуйте LinkedHashSet.
- Значення null.
- HashSet дозволяє зберігати один елемент null.
- TreeSet не дозволяє додавати null без спеціального Comparator, інакше станеться NullPointerException.
5. Типові задачі для Set
Видалення дублікатів зі списку
Припустімо, у нас є список студентів, де частину імен записано двічі. Потрібно залишити лише унікальні імена:
import java.util.*;
public class RemoveDuplicatesDemo {
public static void main(String[] args) {
List<String> students = Arrays.asList("Анна", "Ігор", "Анна", "Марія", "Ігор", "Павло");
Set<String> uniqueStudents = new HashSet<>(students);
System.out.println("Унікальні студенти: " + uniqueStudents);
// Порядок не гарантується!
}
}
Якщо потрібен відсортований результат — використовуйте TreeSet:
Set<String> sortedUniqueStudents = new TreeSet<>(students);
System.out.println("Унікальні студенти (за абеткою): " + sortedUniqueStudents);
Перевірка унікальності (наприклад, логіна користувача)
Set<String> usedLogins = new HashSet<>();
usedLogins.add("student1");
usedLogins.add("java_lover");
String newLogin = "student1";
if (usedLogins.contains(newLogin)) {
System.out.println("Такий логін уже зайнятий!");
} else {
System.out.println("Логін вільний!");
}
Перебирання елементів множини
Перебирання здійснюється за допомогою циклу for-each:
for (String name : uniqueStudents) {
System.out.println(name);
}
6. Типові помилки під час роботи з Set
Помилка № 1: очікування певного порядку елементів у HashSet. Багатьох новачків дивує, чому елементи множини виводяться у «дивному» порядку. Це нормально: HashSet не гарантує порядок. Якщо потрібен порядок додавання — використовуйте LinkedHashSet, якщо потрібне сортування — TreeSet.
Помилка № 2: спроба звернутися до елемента за індексом. Інколи намагаються написати щось на кшталт set.get(0). Так не можна: Set не підтримує індексацію. Потрібен доступ за індексом — беріть List.
Помилка № 3: зберігання змінюваних обʼєктів. Якщо ви зберігаєте обʼєкти, які можуть змінювати поля, що беруть участь у equals()/hashCode(), після зміни таких полів елемент може «загубитися» для множини. Робіть елементи незмінними або не змінюйте ідентифікаційні поля.
Помилка № 4: очікування, що дублікати додаватимуться. Додавання одного й того ж елемента кілька разів не збільшить розмір множини — дублікати ігноруються, метод add поверне false.
Помилка № 5: використання примітивних типів. Запис на кшталт Set<int> не скомпілюється. Використовуйте класи-обгортки: Set<Integer>, Set<Double> тощо.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ