Вопрос 10 · Раздел 4

Что такое Deque?

// Java 21 доступна не везде. Для Java 11/17 используйте deque.peekFirst() вместо getFirst() и deque.peekLast() вместо getLast().

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Deque (Double-Ended Queue) — очередь, где можно добавлять и удалять элементы с обоих концов.

Аналогия: Колода карт — можно брать карту сверху или снизу, можно класть сверху или снизу.

Простая аналогия: Двусторонняя очередь в парке развлечений — можно заходить и выходить с обоих концов.

Deque = Stack + Queue:

Deque<String> deque = new ArrayDeque<>();

// Как стек (LIFO):
deque.push("A");     // Наверх
deque.pop();         // Снять сверху

// Как очередь (FIFO):
deque.offer("A");    // В конец
deque.poll();        // Из начала

// С обоих концов:
deque.addFirst("A"); // В начало
deque.addLast("B");  // В конец
deque.removeFirst(); // Удалить начало
deque.removeLast();  // Удалить конец

🟡 Middle Level

ArrayDeque: лучшая реализация

// Кольцевой массив (Circular Buffer)
Deque<String> deque = new ArrayDeque<>();

// Операции: O(1) amortized
deque.addFirst("A");   // O(1)
deque.addLast("B");    // O(1)
deque.removeFirst();   // O(1)
deque.removeLast();    // O(1)
deque.peekFirst();     // O(1)
deque.peekLast();      // O(1)

Почему ArrayDeque лучше LinkedList

  ArrayDeque LinkedList
Память Массив (мало) Node объекты (много)
Кэш-локальность Отличная Ужасная
Аллокации При ресайзе На каждую вставку
GC pressure Низкий Высокий
Скорость По данным JMH-бенчмарков: ArrayDeque в 3-10 раз быстрее LinkedList при итерации и в 2-5 раз быстрее при вставке/удалении.  

Deque как Stack

// ❌ Не используйте Stack
Stack<String> stack = new Stack<>();

// ✅ Используйте Deque
Deque<String> stack = new ArrayDeque<>();
stack.push("A");
stack.pop();
stack.peek();

Java 21: SequencedCollection

// Java 21 доступна не везде. Для Java 11/17 используйте deque.peekFirst() вместо getFirst() и deque.peekLast() вместо getLast().

// Deque теперь SequencedCollection
deque.getFirst();    // Новый метод!
deque.getLast();     // Новый метод!
deque.reversed();    // Обратный порядок (view)

// Гарантированный порядок обхода
for (String s : deque) { ... }  // От первого к последнему
for (String s : deque.reversed()) { ... }  // Наоборот

🔴 Senior Level

Кольцевой буфер (Circular Array)

Массив: [A][B][C][ ][ ][ ][ ][ ]
         ↑head        ↑tail

addLast(D):
[A][B][C][D][ ][ ][ ][ ]
         ↑head     ↑tail

removeFirst():
[ ][B][C][D][ ][ ][ ][ ]
    ↑head     ↑tail

tail дошёл до конца → заворачивает:
[ ][B][C][D][E][F][ ][ ]
    ↑head              ↑tail

Индекс: (tail + 1) & (length - 1)  // Битовое И!

Битовые маски вместо деления

// Обычный cyclic index:
index = (index + 1) % capacity;  // Деление — медленно!

// ArrayDeque (capacity = степень 2):
index = (index + 1) & (elements.length - 1);  // AND — быстро!

// Пример: capacity = 8 (1000 в бинарном)
// (7 + 1) & 7 = 8 & 7 = 0  ← Завернули!

// Почему степень 2?
// 8 = 1000, 7 = 0111
// x & 7 = x % 8  (для x < 16)
// → Одна CPU инструкция вместо деления!

Trade-off: степень 2 означает что массив может быть заполнен только на 50-75% перед ресайзом (чтобы отличить «полон» от «пуст»). Это немного больше wasted memory, но экономия на делении того стоит.

Почему ArrayDeque запрещает null?

deque.offer(null);  // → NullPointerException!

// Почему?
// poll() и peek() возвращают null для пустой deque
// Если null валидный элемент → нельзя отличить!

// Это architectural decision, не баг!

Concurrency

// ArrayDeque НЕ thread-safe!

// Lock-free Deque:
Deque<String> deque = new ConcurrentLinkedDeque<>();

// Blocking Deque (Work Stealing):
BlockingDeque<String> deque = new LinkedBlockingDeque<>();
// → Потоки могут "воровать" задачи из хвоста
// → ForkJoinPool использует это!

// **Work Stealing** — алгоритм балансировки нагрузки: когда поток завершил свою очередь задач, он «ворует» задачи из хвоста очереди другого потока. Это предотвращает ситуацию, когда один поток перегружен, а другой простаивает. Используется в ForkJoinPool.

Production Experience

Реальный сценарий: LinkedList → ArrayDeque

  • Message queue: LinkedList
  • GC каждые 2 секунды (Node аллокации)
  • Решение: ArrayDeque
  • Результат: GC раз в минуту, +50% throughput

Best Practices

  1. ArrayDeque — для стеков и очередей
  2. Запрещает null — архитектурное ограничение
  3. Степень 2 → битовые маски вместо деления
  4. Java 21 → getFirst(), getLast(), reversed()
  5. ConcurrentLinkedDeque — lock-free
  6. LinkedBlockingDeque — Work Stealing
  7. НЕ используйте Stack или LinkedList

Резюме для Senior

  • Deque = двухсторонняя очередь (LIFO + FIFO)
  • ArrayDeque = кольцевой массив, capacity = 2^n
  • Битовые маски → (index + 1) & (length - 1)
  • null запрещён → poll/peek однозначны
  • Кэш-локальность » LinkedList
  • SequencedCollection (Java 21) → getFirst/reversed
  • Work Stealing → LinkedBlockingDeque
  • Никогда не используйте Stack

🎯 Шпаргалка для интервью

Обязательно знать:

  • Deque (Double-Ended Queue) = двухсторонняя очередь, поддержка LIFO + FIFO
  • ArrayDeque — лучшая реализация: кольцевой массив, O(1) amortized для всех операций с обоих концов
  • ArrayDeque лучше LinkedList: в 3-10 раз быстрее при итерации, в 2-5 раз при вставке/удалении, меньше GC pressure
  • Кольцевой буфер: capacity = степень 2 → битовые маски вместо деления ((index + 1) & (length - 1))
  • ArrayDeque запрещает null — архитектурное решение (poll/peek возвращают null для пустой deque)
  • Java 21: SequencedCollection → getFirst(), getLast(), reversed() для Deque
  • Concurrency: ConcurrentLinkedDeque (lock-free), LinkedBlockingDeque (Work Stealing, ForkJoinPool)
  • ArrayDeque заменяет Stack: push/pop/peek — те же методы, без synchronized и LSP violation

Частые уточняющие вопросы:

  • Почему ArrayDeque использует capacity = степень 2? — Для битовых масок: (index + 1) & (length - 1) вместо медленного деления %
  • Почему ArrayDeque запрещает null? — poll()/peek() возвращают null для пустой deque, null элемент создал бы двусмысленность
  • Что такое Work Stealing? — Потоки «воруют» задачи из хвоста чужой очереди, балансировка нагрузки в ForkJoinPool
  • Чем Deque отличается от Queue? — Deque поддерживает операции с обоих концов, Queue — только с одного (FIFO)

Красные флаги (НЕ говорить):

  • ❌ “LinkedList — хорошая реализация Deque” — ArrayDeque всегда лучше (кэш-локальность, меньше памяти)
  • ❌ “ArrayDeque можно использовать как Map ключ” — Deque не имеет hashCode/equals контракт для ключей
  • ❌ “Deque может содержать null” — ArrayDeque запрещает null, это architectural decision
  • ❌ “Используйте Stack как стек” — ArrayDeque заменяет Stack без synchronized оверхеда и LSP violation

Связанные темы:

  • [[Что такое Queue и какие реализации существуют]]
  • [[Что такое Stack]]
  • [[В чём разница между ArrayList и LinkedList]]
  • [[Какие основные интерфейсы Collection Framework]]
  • [[Что такое Vector и чем он отличается от ArrayList]]