Когда использовать ArrayList, а когда LinkedList?
4. ensureCapacity() при известном размере 5. Избегайте LinkedList без веской причины 6. Кэш-локальность > алгоритмическая сложность 7. GC pressure учитывайте при выборе
🟢 Junior Level
Короткий ответ: В подавляющем большинстве случаев используйте ArrayList.
Исключения для LinkedList: (1) Удаление через итератор в списках 100к+ элементов, где нельзя использовать removeIf() (Java 8+). (2) Специализированные структуры данных (LRU-кэш на основе двусвязного списка).
// ✅ В 99% случаев:
List<String> list = new ArrayList<>();
// ❌ LinkedList — не используйте без причины
List<String> list = new LinkedList<>();
Единственная причина для LinkedList:
- Очень большой список (миллионы элементов)
- И нужно удалять элементы через итератор во время обхода
Для очереди/стека используйте ArrayDeque:
// ❌ LinkedList как очередь
Deque<String> queue = new LinkedList<>();
// ✅ ArrayDeque (быстрее!)
Deque<String> queue = new ArrayDeque<>();
🟡 Middle Level
ArrayList: когда использовать
// ✅ Чтение по индексу
list.get(100); // O(1) — мгновенно
// ✅ Итерация (перебор всех элементов)
for (String s : list) { ... } // Быстро (кэш-локальность)
// ✅ Добавление в конец
list.add(e); // O(1)* — амортизированное
// ✅ Если известен размер
ArrayList<String> list = new ArrayList<>(10000);
// → Без ресайзов
LinkedList: когда (редко) использовать
// ⚠️ Единственный кейс:
// 1. Огромный список (100,000+ элементов)
// 2. Частое удаление ЧЕРЕЗ ИТЕРАТОР
// 3. В процессе обхода
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (shouldRemove(it.next())) {
it.remove(); // O(1) для LinkedList, O(n) для ArrayList
}
}
// НО! В Java 8+ лучше использовать:
list.removeIf(this::shouldRemove); // O(n) для обоих
ArrayDeque: для очередей и стеков
// Стек (LIFO)
Deque<String> stack = new ArrayDeque<>();
stack.push("A");
stack.pop();
// Очередь (FIFO)
Deque<String> queue = new ArrayDeque<>();
queue.offer("A");
queue.poll();
// → Быстрее LinkedList, меньше памяти
Сравнение на практике
| Сценарий | ArrayList | LinkedList | ArrayDeque |
|---|---|---|---|
| Чтения много | ✅ | ❌ | Н/Д |
| Вставка в конец | ✅ | ✅ | ✅ |
| Вставка в начало | ❌ | ✅ | ✅ |
| Удаление по индексу | ❌ | ❌ | Н/Д |
| Удаление итератором | ❌ | ✅ | Н/Д |
| Память | ✅ | ❌ | ✅ |
| GC pressure | ✅ | ❌ | ✅ |
🔴 Senior Level
Cache Locality — главный аргумент
CPU Cache Lines: 64 байта
Cache line — минимальный блок памяти, который CPU загружает из RAM в кэш. Даже если вам нужен 1 байт, CPU загрузит все 64 байта вокруг него. Поэтому соседние данные в массиве «бесплатны».
ArrayList:
[ref1][ref2][ref3][ref4][ref5][ref6][ref7][ref8]
→ Одно чтение → 8 ссылок в L1 кэше
→ Следующие 7 элементов = 0 задержек!
LinkedList:
[Node1]......[Node2]......[Node3]
↓ ↓ ↓
RAM access: 100+ cycles EACH
→ Cache miss на КАЖДОМ элементе!
System.arraycopy vs Node Creation
// ArrayList.add(0, e):
// → System.arraycopy() → SIMD инструкции
// → 1000 элементов → ~1 мкс
// LinkedList.add(0, e):
// → new Node() → аллокация в Heap
// → 3 ссылки (item, next, prev)
// → 1000 элементов → ~5 мкс + GC overhead
// ArrayList быстрее до ~10-20к элементов!
// (По данным бенчмарков JMH на современных JVM. Конкретный порог зависит от CPU, версии JVM и паттерна доступа.)
GC Pressure Deep Dive
LinkedList: 1 млн элементов
→ 1 млн Node объектов
→ 32 МБ только оверхеда
→ Minor GC: сканировать 1 млн объектов
→ STW паузы!
ArrayList: 1 млн элементов
→ 1 Object[] array
→ 4 МБ
→ GC: одна ссылка → мгновенно
removeIf оптимизация
// ❌ Ручное удаление через итератор
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
if (condition(it.next())) {
it.remove(); // O(n) для ArrayList (сдвиг каждый раз)
}
}
// ✅ removeIf (Java 8+)
list.removeIf(e -> condition(e));
// → Сначала находит все для удаления (O(n))
// → Потом ОДИН сдвиг (O(n))
// → Итого: O(n) вместо O(n²)!
When ArrayList also loses
// Частые вставки в НАЧАЛО большого списка (> 100к элементов)
// ArrayList: каждый раз сдвиг 100к элементов
// Решение:
// 1. ArrayDeque (если вставка с обоих концов)
// 2. Или два ArrayList (head + tail)
// 3. Или пересмотреть архитектуру
Production Experience
Реальный сценарий #1: LinkedList → ArrayList
- Лог: 500,000 строк в LinkedList
- Память: 800 МБ
- Итерация: 5 секунд
- Решение: ArrayList
- Результат: 80 МБ, 100 мс (ускорение в 50 раз!)
Реальный сценарий #2: LinkedList как очередь
- Message queue: LinkedList
- GC: каждые 2 секунды (создание Node)
- Решение: ArrayDeque
- Результат: GC раз в минуту
Best Practices
- ArrayList — 99% случаев
- ArrayDeque — очереди/стеки
- removeIf() вместо итерации с удалением
- ensureCapacity() при известном размере
- Избегайте LinkedList без веской причины
- Кэш-локальность > алгоритмическая сложность
- GC pressure учитывайте при выборе
Резюме для Senior
- ArrayList — выбор по умолчанию (кэш-локальность)
- LinkedList — антипаттерн для современных CPU
- ArrayDeque — для стеков и очередей
- removeIf() > итерация с удалением
- Cache Locality > Big O в реальности
- GC Pressure — скрытая стоимость LinkedList
- System.arraycopy быстрее создания Node
- Valhalla (будущее): Inline Types увеличат разрыв
🎯 Шпаргалка для интервью
Обязательно знать:
- ArrayList — выбор по умолчанию для 99% сценариев
- Кэш-локальность > алгоритмическая сложность: CPU загружает 64-байт линию кэша за раз, получая 8-16 ссылок «бесплатно»
- System.arraycopy() = SIMD инструкции — быстрее чем new Node() для списков до 10-20к элементов
- removeIf() лучше ручного удаления через итератор: один проход + один сдвиг = O(n) вместо O(n²)
- ArrayDeque — для стеков и очередей (быстрее LinkedList, меньше памяти, нет GC pressure)
- GC Pressure: LinkedList 1 млн элементов = 32 МБ мусора, ArrayList = 0 новых объектов
- ensureCapacity() экономит 30+ ресайзов при известном размере
- Реальный кейс: замена LinkedList → ArrayList дала ускорение в 50-200 раз и снижение памяти в 10 раз
Частые уточняющие вопросы:
- Когда LinkedList может выиграть? — Частые вставки в НАЧАЛО списка >100к элементов; удаление через итератор в огромных списках
- Что такое cache line и почему это важно? — 64-байтный блок, CPU загружает его целиком; ArrayList использует это, LinkedList — нет
- Почему removeIf() быстрее цикла с итератором? — Сначала находит все элементы для удаления, потом ОДИН сдвиг массива
- Когда ArrayList тоже проигрывает? — Частые вставки в начало большого списка (>100к) — тогда ArrayDeque
Красные флаги (НЕ говорить):
- ❌ “LinkedList лучше для частых вставок” — ArrayList быстрее до 10-20к элементов благодаря SIMD
- ❌ “Для очереди используйте LinkedList” — ArrayDeque всегда лучше (быстрее, меньше памяти)
- ❌ “Big O определяет производительность” — кэш-локальность и константы важнее на практике
- ❌ “Удаление через итератор — нормальная практика” — используйте removeIf() в Java 8+
Связанные темы:
- [[В чём разница между ArrayList и LinkedList]]
- [[Какова временная сложность операций в ArrayList]]
- [[Какова временная сложность операций в LinkedList]]
- [[Что такое Deque]]
- [[Что такое Vector и чем он отличается от ArrayList]]