JavaRush /Курси /JAVA 25 SELF /Set: HashSet і TreeSet, унікальність елементів

Set: HashSet і TreeSet, унікальність елементів

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

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> тощо.

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