Питання 30 · Розділ 4

Які операції підтримує інтерфейс Collection?

Якщо проект на Java 8 — використовуйте coll.toArray(new String[0]). JVM оптимізує це так само ефективно.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Collection — базовий інтерфейс для List, Set, Queue.

⚠️ НЕ плутати: Collection (без ‘s’) — інтерфейс (List, Set, Queue). Collections (з ‘s’) — утилітарний клас зі static-методами (sort, synchronizedList, unmodifiableList). Це різні речі!

Основні методи:

Collection<String> coll = new ArrayList<>();

// Додавання
coll.add("A");
coll.addAll(List.of("B", "C"));

// Перевірка
coll.contains("A");       // true
coll.containsAll(...);    // true
coll.isEmpty();           // false
coll.size();              // 3

// Видалення
coll.remove("A");
coll.removeAll(...);
coll.clear();

// Перетворення
Object[] arr = coll.toArray();
String[] strArr = coll.toArray(String[]::new);  // Java 11+

🟡 Middle Level

Групи методів

Модифікація:

add(e)           // → true якщо змінилась
addAll(c)        // → true якщо змінилась
remove(e)        // → true якщо видалив
removeAll(c)     // Видалити всі з c
retainAll(c)     // Залишити тільки з c (перетин)
clear()          // Очистити

Java 8+ функціональні:

// removeIf — видалення за умовою
list.removeIf(e -> e.startsWith("A"));  // O(n) для ArrayList!

// stream / parallelStream
coll.stream()
    .filter(e -> e.length() > 3)
    .map(String::toUpperCase)
    .collect(Collectors.toList());

Складність contains

// ArrayList: O(n)
list.contains("A");  // Перебір!

// HashSet: O(1)
set.contains("A");   // Хеш!

// containsAll для двох списків: O(n × m)!
list1.containsAll(list2);  // Для кожного з list2 перебір list1!

toArray

// Java 11+: найсучасніший спосіб
String[] arr = list.toArray(String[]::new);

// Старий спосіб (теж працює)
String[] arr = list.toArray(new String[0]);
// → JVM оптимізує, не потрібно створювати масив потрібного розміру!

Якщо проект на Java 8 — використовуйте coll.toArray(new String[0]). JVM оптимізує це так само ефективно.


🔴 Senior Level

Коли НЕ використовувати parallelStream

  1. Колекція < 10K елементів — overhead форка > вигода
  2. Операції швидкі (арифметика, getters)
  3. Порядок важливий — parallelStream не гарантує порядок
  4. Є side-ефекти — race conditions

removeIf оптимізація

// ArrayList.removeIf:
// 1. Знаходить перший елемент для видалення
// 2. До нього — НЕ чіпає
// 3. Після — ОДИН System.arraycopy
// → O(n), мінімальні записи!

// VS iterator.remove() в циклі:
// → System.arraycopy КОЖЕН раз
// → O(n²)!

Bulk operations складність

// list1.removeAll(list2)
// Якщо list2 = ArrayList: O(n × m)
// Якщо list2 = HashSet: O(n + m)

// → Завжди конвертуйте в Set для bulk операцій!
Set<String> set = new HashSet<>(list2);
list1.removeAll(set);  // O(n + m)

Для середніх і великих колекцій — конвертація в Set вигідна. Для маленьких (< 10 елементів) overhead створення HashSet може перевищити вигоду.

Spliterator характеристики

Spliterator<String> spliterator = coll.spliterator();

// Прапорці оптимізації:
// IMMUTABLE  → колекція не змінюється
// CONCURRENT → можна змінювати паралельно
// DISTINCT   → всі елементи унікальні
// SORTED     → відсортовано
// SIZED      → відомий точний розмір

// Stream API використовує ці прапорці!
set.stream()
   .distinct()  // → Нічого не робить! Вже DISTINCT
   .count();    // → O(1) тільки коли Stream знає точний розмір і немає операцій, що змінюють кількість елементів. distinct() все одно вимагає обходу.

GC Pressure

// toArray() створює КОПІЮ всіх даних!
Object[] copy = collection.toArray();  // O(n) пам'ять

// Для великих колекцій → GC тиск
// → Використовуйте stream замість копіювання
collection.stream().forEach(this::process);

toArray створює повну копію. Уникайте якщо: (1) Колекція > 100K елементів (GC pressure). (2) Ви тільки читаєте (використовуйте stream/forEach). Припустимо якщо: (1) Потрібен масив для legacy API. (2) Колекція маленька.

Production Experience

Реальний сценарій: containsAll O(n²)

// 10,000 елементів в обох списках
list1.containsAll(list2);  // 10,000 × 10,000 = 100 млн операцій!

// Рішення:
Set<String> set = new HashSet<>(list2);
list1.containsAll(set);  // 10,000 × O(1) = 10,000 операцій
// → Прискорення в 10,000 разів!

Best Practices

  1. removeIf → O(n) для ArrayList
  2. toArray(T[]::new) → Java 11+ стиль
  3. containsAll з Set → O(n+m) замість O(n×m)
  4. Spliterator характеристики → оптимізації Stream
  5. Уникайте toArray для великих колекцій (GC)
  6. retainAll = перетин множин
  7. addAll = об’єднання

Резюме для Senior

  • removeIf = O(n), оптимізований всередині
  • Bulk ops → конвертуйте в Set
  • toArray → копія, GC pressure
  • Spliterator → прапорці для Stream оптимізацій
  • contains → O(n) для List, O(1) для Set
  • Java 11+ → toArray(T[]::new)

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • Collection (інтерфейс) != Collections (утилітарний клас) — не плутати на інтерв’ю
  • Базові групи: модифікація (add, remove, retainAll), перевірка (contains, isEmpty), перетворення (stream, toArray)
  • removeIf() — O(n) для ArrayList (один System.arraycopy), краще iterator.remove() O(n^2)
  • containsAll/removeAll для двох ArrayList = O(n × m); конвертація одного в Set = O(n + m)
  • toArray(T[]::new) (Java 11+) — сучасний стиль; JVM оптимізує new T[0] так само ефективно
  • toArray() створює повну копію — GC pressure для великих колекцій (>100K елементів)
  • Spliterator прапорці (DISTINCT, SORTED, SIZED) — внутрішні оптимізації Stream API

Часті уточнюючі запитання:

  • Чому containsAll двох списків з 10K елементів = 100 млн операцій? — Для кожного елемента list2 перебирається list1: 10K × 10K. Рішення: new HashSet(list2).
  • Коли parallelStream НЕ варто використовувати? — Колекція < 10K елементів, швидкі операції, важливий порядок, є side-ефекти.
  • Навіщо конвертувати в Set перед removeAll? — HashSet.contains = O(1); замість O(n × m) отримуємо O(n + m).
  • Що роблять прапорці Spliterator? — Підказки Stream API для оптимізації: DISTINCT — distinct() noop, SIZED — count() = O(1).

Червоні прапорці (НЕ говорити):

  • «Collection і Collections — одне і те ж» — ні, Collection = інтерфейс, Collections = утилітарний клас
  • «contains для ArrayList = O(1)» — ні, O(n), перебір всіх елементів
  • «toArray() не створює копію» — ні, це повна копія даних в новий масив
  • «parallelStream завжди швидший за звичайний stream» — ні, overhead форка для малих колекцій перевищує вигоду

Пов’язані теми:

  • [[28. Як правильно видаляти елементи під час ітерації]]
  • [[23. Що таке Collections.unmodifiableList()]]
  • [[26. Що таке fail-fast та fail-safe ітератори]]