В чому різниця між List, Set та Queue?
Кеш-локальність = елементи розташовані поруч у пам'яті. CPU завантажує 64-байтну лінію кешу за один раз, отримуючи одразу 8-16 посилань. Це прискорює ітерацію у 10-100 разів пор...
🟢 Junior Level
List, Set та Queue — три основні типи колекцій у Java.
Головна різниця:
| Тип | Порядок | Дублікати | Доступ | Аналогія |
|---|---|---|---|---|
| List | Зберігає порядок | ✅ Можна | За індексом list.get(0) |
Список покупок |
| Set | Не гарантує (крім LinkedHashSet) | ❌ Не можна | Тільки перебор | Набір ключів |
| Queue | FIFO (First-In-First-Out — перший доданий елемент буде першим вилучений) | ✅ Можна | Тільки голову | Черга в магазині |
Приклад:
// List: порядок та дублікати
List<String> list = new ArrayList<>();
list.add("B");
list.add("A");
list.add("B"); // OK
// → [B, A, B]
// Set: тільки унікальні
Set<String> set = new HashSet<>();
set.add("B");
set.add("A");
set.add("B"); // НЕ додасться
// → [A, B] (порядок не гарантований)
// Queue: FIFO
Queue<String> queue = new ArrayDeque<>();
queue.offer("B");
queue.offer("A");
queue.poll(); // → "B" (перший прийшов, перший пішов)
🟡 Middle Level
List (Послідовність)
// Реалізації:
ArrayList // Динамічний масив → O(1) читання
LinkedList // Двозв'язний список → O(n) читання
// Операції:
list.get(0) // O(1) для ArrayList, O(n) для LinkedList
list.add(e) // O(1)* амортизоване
list.contains(e) // O(n) лінійний пошук
list.remove(0) // O(n) зсув масиву
Set (Множина)
// Реалізації:
HashSet // На базі HashMap → O(1) пошук
LinkedHashSet // + порядок додавання
TreeSet // Червоно-чорне дерево → O(log n), відсортований
// Всередині HashSet:
// HashMap<E, Object> де value = static final Object PRESENT
// → Попри один спільний об'єкт PRESENT, HashSet витрачає більше пам'яті, ніж ArrayList, через обгортку в HashMap.Node (ключ + value + hash + next pointer).
// Операції:
set.add(e) // O(1) для HashSet, O(log n) для TreeSet
set.contains(e) // O(1) для HashSet, O(log n) для TreeSet
set.remove(e) // O(1) для HashSet
Queue (Черга)
// Реалізації:
ArrayDeque // Кільцевий масив → O(1) вставка/видалення
PriorityQueue // Binary Heap → O(log n), вилучення за пріоритетом
LinkedList // Можна як Queue, але повільніший за ArrayDeque
// Методи:
queue.offer(e) // true/false (не кидає exception)
queue.add(e) // кидає exception при помилці
queue.poll() // null якщо пуста
queue.remove() // кидає exception якщо пуста
queue.peek() // null якщо пуста
queue.element() // кидає exception якщо пуста
Порівняння продуктивності
| Операція | ArrayList | HashSet | TreeSet | ArrayDeque |
|---|---|---|---|---|
| Додавання | O(1)* | O(1) | O(log n) | O(1) |
| Пошук | O(n) | O(1) | O(log n) | O(n) |
| Видалення | O(n) | O(1) | O(log n) | O(1) |
| Доступ за індексом | O(1) | Н/Д | Н/Д | Н/Д |
Коли що використовувати
| Сценарій | Що обрати | Чому |
|---|---|---|
| Зберігання з порядком | List | Зберігає порядок |
| Перевірка унікальності | Set | Швидкий contains |
| Обробка по порядку | Queue | FIFO |
| Сортування | TreeSet | Автоматичне сортування |
| Часті читання | ArrayList | Кеш-локальність |
Кеш-локальність = елементи розташовані поруч у пам’яті. CPU завантажує 64-байтну лінію кешу за один раз, отримуючи одразу 8-16 посилань. Це прискорює ітерацію у 10-100 разів порівняно з розкиданими елементами LinkedList.
Коли НЕ використовувати
- List (ArrayList): Не використовуйте для перевірки унікальності —
contains()= O(n) - HashSet: Не використовуйте якщо важливий порядок вставки — візьміть
LinkedHashSet - TreeSet: Не використовуйте якщо не потрібне сортування — O(log n) повільніше за O(1)
- Queue: Не використовуйте для довільного доступу — візьміть
List
🔴 Senior Level
Memory Footprint
ArrayList:
→ Object[] array (4-8 байт на посилання)
→ Мінімальний оверхед
HashSet:
→ HashMap всередині
→ Node<K,V> для кожного елемента
→ + static final Object PRESENT (один на всі)
→ ~2x більше пам'яті, ніж ArrayList!
TreeSet:
→ TreeMap (червоно-чорне дерево)
→ Entry<K,V> + left + right + parent + color
→ ~4x більше пам'яті, ніж ArrayList
Concurrency
// List
CopyOnWriteArrayList // Багато читань, мало записів
Collections.synchronizedList // Універсальний, але повільний
// Set
ConcurrentHashMap.newKeySet() // Найкращий вибір!
Collections.synchronizedSet()
// Queue
ConcurrentLinkedQueue // Lock-free, wait-free
LinkedBlockingQueue // Producer-Consumer
ArrayBlockingQueue // Bounded, один лок
SynchronousQueue // Нульова ємність
Cache Locality
ArrayList: елементи поруч у пам'яті
→ CPU prefetching → L1/L2 cache hits
→ Ітерація: дуже швидко
HashSet: елементи розкидані
→ Cache misses при ітерації
→ Але O(1) contains компенсує
LinkedList: вузли в різних кінцях купи
→ Cache miss на КОЖНОМУ елементі
→ Антипатерн для сучасних CPU!
Java 21: SequencedCollection
// Тепер List та Deque наслідують SequencedCollection
SequencedCollection<String> seq = new ArrayList<>();
seq.addFirst("A"); // Новий метод!
seq.addLast("B"); // Новий метод!
seq.getFirst(); // Новий метод!
seq.reversed(); // Відображення у зворотному порядку
// Для Set:
SequencedSet<String> seqSet = new LinkedHashSet<>();
// TreeSet теж підтримує через NavigableSet
Bloom Filter для величезних Set
// Коли елементів мільярди, HashSet не вміщується в RAM
// Рішення: Bloom Filter (стороння структура даних, не входить у JCF)
// «False positive» = фільтр може сказати «елемент можливо є» коли його насправді немає.
// Але «false negative» неможливий: якщо фільтр каже «елемента немає» — він точно відсутній.
BloomFilter<String> filter = BloomFilter.create(
Funnels.stringFunnel(),
expectedInsertions,
falsePositiveRate
);
// O(1) пошук, мінімальна пам'ять
// Але: можливі false positives (немає false negatives)
Production Experience
Реальний сценарій: HashSet vs ArrayList для contains
// ❌ O(n) пошук у списку з 1 млн елементів
List<String> blacklist = List.of("bad1", "bad2", ...);
if (blacklist.contains(input)) { ... } // Повільно!
// ✅ O(1) пошук
Set<String> blacklist = Set.of("bad1", "bad2", ...);
if (blacklist.contains(input)) { ... } // Швидко!
Best Practices
- List → ArrayList за замовчуванням
- Set → HashSet для унікальності, TreeSet для сортування
- Queue → ArrayDeque для стека/черги
- Concurrency → ConcurrentHashMap.newKeySet() для Set
- Пам’ять → ArrayList компактніший за Set
- Cache Locality → ArrayList > LinkedList
- Java 21 → SequencedCollection для загального інтерфейсу
Резюме для Senior
- List = індексація, порядок, кеш-локальність
- Set = унікальність, O(1) contains (HashSet)
- Queue = FIFO/LIFO, backpressure (Bounded)
- Пам’ять: ArrayList < HashSet < TreeSet
- Cache Locality: ArrayList » LinkedList
- Concurrency: CopyOnWriteArrayList, ConcurrentHashMap.newKeySet()
- Bloom Filter для величезних множин
- Java 21 → SequencedCollection уніфікує API
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- List = порядок + дублікати + доступ за індексом (ArrayList O(1) читання, LinkedList O(n))
- Set = унікальність через equals()/hashCode(), HashSet = O(1) пошук, TreeSet = O(log n) сортування
- Queue = FIFO, методи offer/poll/peek безпечніші за add/remove/element
- HashSet всередині = HashMap<E, Object> зі спільним PRESENT, ~2x більше пам’яті ніж ArrayList
- Кеш-локальність: ArrayList » LinkedList — елементи поруч у пам’яті, CPU prefetching
- Для concurrency: CopyOnWriteArrayList (багато читань), ConcurrentHashMap.newKeySet() для Set
- Bloom Filter — для величезних множин (мільярди елементів), O(1) пошук, можливі false positives
Часті уточнюючі запитання:
- Чому HashSet споживає більше пам’яті ніж ArrayList? — Всередині HashMap.Node (ключ + value + hash + next pointer)
- Коли використовувати TreeSet замість HashSet? — Коли потрібне сортування, але O(log n) повільніше за O(1)
- Чим offer/poll відрізняються від add/remove? — offer/poll повертають false/null замість exception
- Як забезпечити thread-safety для Set? — ConcurrentHashMap.newKeySet() — найкращий вибір
Червоні прапорці (НЕ говорити):
- ❌ “LinkedList швидший за ArrayList для ітерації” — навпаки, кеш-локальність ArrayList дає перевагу у 10-100 разів
- ❌ “HashSet гарантує порядок вставки” — це LinkedHashSet, звичайний HashSet не гарантує порядок
- ❌ “Для thread-safe Set потрібен Collections.synchronizedSet” — ConcurrentHashMap.newKeySet() ефективніше
- ❌ “Queue та List взаємозамінні” — Queue не підтримує довільний доступ за індексом
Пов’язані теми:
- [[Які основні інтерфейси Collection Framework]]
- [[В чому різниця між ArrayList та LinkedList]]
- [[Яка часова складність операцій в ArrayList]]
- [[Що таке Queue і які реалізації існують]]