Коллекции ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-10-24

Класс ArrayDeque в Java

Сравните две очереди с одинаковыми элементами: new ArrayDeque<>(List.of(1, 2, 3)).equals(new ArrayDeque<>(List.of(1, 2, 3))) вернёт false. ArrayDeque не переопределяет equals(), поэтому сравнивает ссылки, а не содержимое — на этом падают unit-тесты и поиск деки в HashSet. Это одна из особенностей реализации, о которых ничего не говорит интерфейс Deque.

ArrayDeque (java.util.ArrayDeque, array deque — «двусторонняя очередь на массиве») — класс из Java 6, который реализует интерфейс Deque на кольцевом массиве с автоматическим ростом. Добавление и удаление на обоих концах выполняется за амортизированное O(1), поэтому ArrayDeque — стандартный выбор для стека и очереди FIFO в однопоточном коде. Элементы null запрещены, класс не потокобезопасен.

Методы интерфейсов Queue и Deque — offer(), poll(), peek(), push(), pop() и их варианты с First/Last — разобраны в уроке Интерфейсы Queue и Deque. Здесь речь о том, как они работают именно в ArrayDeque.

Как устроен ArrayDeque: кольцевой массив

Внутри ArrayDeque лежит обычный массив Object[] и два индекса: head (голова) и tail (хвост). Массив замкнут в кольцо: если при добавлении в начало индекс уходит левее нуля, элемент записывается в последнюю ячейку. Поэтому добавление и удаление с обоих концов не сдвигает остальные элементы.

Так выглядит условный массив из 8 ячеек после вызовов addLast("A"), addLast("B"), addLast("C") и addFirst("Z"):

индекс:   0     1     2     3     4     5     6     7
        [ A ] [ B ] [ C ] [   ] [   ] [   ] [   ] [ Z ]
                          ^ tail                  ^ head

addFirst() сдвинул head с 0 на 7, «обернувшись» через конец массива, а addLast() пишет в ячейку tail и сдвигает его вправо. Логический порядок элементов — от head по кругу до tail:

import java.util.ArrayDeque;
import java.util.Deque;

public class RingBufferDemo {
    public static void main(String[] args) {
        Deque<String> deque = new ArrayDeque<>();
        deque.addLast("A");
        deque.addLast("B");
        deque.addLast("C");
        deque.addFirst("Z");

        System.out.println(deque);                                    // [Z, A, B, C]
        System.out.println(deque.peekFirst() + " " + deque.peekLast()); // Z C
    }
}

Вывод:

[Z, A, B, C]
Z C

Когда массив заполняется, ArrayDeque создаёт новый массив побольше и копирует в него элементы — это O(n), но происходит редко, поэтому в среднем добавление остаётся O(1). Именно этим ArrayDeque отличается от ArrayList: тот всегда начинает данные с индекса 0, и remove(0) сдвигает весь массив.

Конструкторы и ёмкость ArrayDeque

У ArrayDeque три конструктора: пустой с начальной ёмкостью 16 элементов, с заданной начальной ёмкостью и копирующий элементы другой коллекции. Ёмкость — это размер внутреннего массива, а не ограничение: при заполнении дека растёт автоматически.

Конструктор Что создаёт Граничный случай
ArrayDeque() Пустую двустороннюю очередь, в которую без расширения помещается 16 элементов —
ArrayDeque(int numElements) Пустую очередь с массивом под numElements элементов Отрицательное или нулевое значение не вызывает исключения: new ArrayDeque<>(-5) создаёт дек минимальной ёмкости (а new ArrayList<>(-5) бросит IllegalArgumentException)
ArrayDeque(Collection<? extends E> c) Очередь из элементов c в том порядке, в котором их возвращает итератор c Если в c есть null (например, Arrays.asList(1, null)) — NullPointerException

Как именно растёт массив, зависит от версии JDK. В Java 8 ёмкость всегда округлялась до степени двойки (минимум 8) и при заполнении удваивалась. В JDK 17 и 21 конструктор выделяет место ровно под numElements элементов, а при росте массив удваивается, пока он меньше 64 ячеек, и дальше увеличивается на 50%. В обратную сторону массив не уменьшается: после удаления элементов память под ячейки остаётся занятой.

Начальная ёмкость — не лимит

new ArrayDeque<>(100) не создаёт очередь на 100 элементов: 101-й добавится так же успешно, просто массив будет пересоздан. Число в конструкторе имеет смысл передавать, когда размер заранее известен, — это избавляет от лишних копирований. Если нужен настоящий предел, берут очередь ограниченной ёмкости из java.util.concurrent (пример — в разделе Реализации Queue и Deque).

Пример ArrayDeque: очередь и стек

Один и тот же класс ArrayDeque работает и как стек LIFO, и как очередь FIFO — всё зависит от вызываемых методов: push()/pop() работают с головой, add()/remove() добавляют в хвост и забирают из головы. Переменную объявляют типом интерфейса Deque или Queue.

import java.util.ArrayDeque;
import java.util.Deque;

public class ArrayDequeExample {
    public static void main(String[] args) {
        Deque<String> stack = new ArrayDeque<>();
        Deque<String> queue = new ArrayDeque<>(2);
        stack.push("A");
        stack.push("B");
        stack.push("C");
        stack.push("D");
        while (!stack.isEmpty()) {
            System.out.print(stack.pop() + " ");
        }
        System.out.println();

        queue.add("A");
        queue.add("B");
        queue.add("C");
        queue.add("D");
        while (!queue.isEmpty()) {
            System.out.print(queue.remove() + " ");
        }
    }
}

Вывод:

D C B A
A B C D

Стек вернул элементы в обратном порядке: последний добавленный "D" вышел первым. Очередь сохранила порядок добавления. Обратите внимание: queue создана с начальной ёмкостью 2, но без ошибок приняла четыре элемента — массив вырос сам.

ArrayDeque как стек: проверка парности скобок

Классическая задача для стека — проверить, что скобки в строке правильно вложены. Открывающую скобку кладём в стек, на закрывающей снимаем вершину и проверяем, что она из той же пары:

import java.util.ArrayDeque;
import java.util.Deque;

public class BracketChecker {
    static boolean isBalanced(String s) {
        Deque<Character> stack = new ArrayDeque<>();
        for (char c : s.toCharArray()) {
            if (c == '(' || c == '[' || c == '{') {
                stack.push(c);
            } else if (c == ')' || c == ']' || c == '}') {
                if (stack.isEmpty()) {
                    return false;          // закрывающая скобка без пары
                }
                char open = stack.pop();
                if ((c == ')' && open != '(')
                        || (c == ']' && open != '[')
                        || (c == '}' && open != '{')) {
                    return false;          // скобки разных видов
                }
            }
        }
        return stack.isEmpty();            // остались незакрытые скобки?
    }

    public static void main(String[] args) {
        System.out.println(isBalanced("list.get(map[key])"));
        System.out.println(isBalanced("{[}]"));
        System.out.println(isBalanced("(("));
        System.out.println(isBalanced(")("));
    }
}

Вывод:

true
false
false
false

Проверка stack.isEmpty() перед pop() обязательна: для строки ")(" без неё pop() на пустом стеке бросил бы NoSuchElementException. Как очередь ArrayDeque используют, например, в обходе дерева или графа в ширину: offer() в хвост, poll() из головы.

Методы ArrayDeque и их сложность

ArrayDeque реализует все методы Deque, Queue и Collection; собственный публичный метод сверх интерфейсов у него один — clone(). Операции на концах выполняются за O(1), а всё, что требует поиска элемента внутри, — за O(n). Методов с индексом (get(int), set(int, E)) нет.

Методы Сложность Пустая дека, null и другие граничные случаи
addFirst(), addLast(), offerFirst(), offerLast(), push(), add(), offer() O(1) амортизированно; O(n) в момент роста массива null — NullPointerException; offer*() всегда возвращают true
pollFirst(), pollLast(), poll(), removeFirst(), removeLast(), pop(), remove() O(1) Пусто: poll*() — null, remove*() и pop() — NoSuchElementException
peekFirst(), peekLast(), peek(), getFirst(), getLast(), element() O(1) Пусто: peek*() — null, get*() и element() — NoSuchElementException
size(), isEmpty() O(1) —
contains(o), remove(o), removeFirstOccurrence(o), removeLastOccurrence(o) O(n) — перебор от головы или хвоста Для null возвращают false, исключения нет
iterator().remove(), removeIf() O(n) — удаление из середины сдвигает элементы remove() до next() — IllegalStateException
clone(), toArray() O(n) Копируется массив, но не сами элементы (поверхностная копия); порядок — от головы к хвосту

Что делает каждый метод и чем отличаются пары offer/add и poll/remove, показано в таблицах урока Интерфейс Deque в Java.

null, contains() и equals(): неочевидные моменты

ArrayDeque не хранит null, потому что пустые ячейки кольцевого массива как раз и отмечены значением null, а poll() и peek() возвращают null как сигнал «дека пуста». Кроме того, класс не переопределяет equals() и hashCode(), поэтому сравнивает деки по ссылке.

Запрет действует только на запись: offer(null), push(null) и addLast(null) бросают NullPointerException, а contains(null) и remove(null) спокойно возвращают false. Почему null в очереди опасен и как на нём ломается цикл с poll(), разобрано в разделе Queue и null.

Что выведет этот код?

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;

public class DequeEquals {
    public static void main(String[] args) {
        Deque<Integer> a = new ArrayDeque<>(List.of(1, 2, 3));
        Deque<Integer> b = new ArrayDeque<>(List.of(1, 2, 3));

        System.out.println(a.equals(b));
        System.out.println(List.copyOf(a).equals(List.copyOf(b)));
        System.out.println(Arrays.equals(a.toArray(), b.toArray()));
        System.out.println(new LinkedList<>(a).equals(new LinkedList<>(b)));
    }
}

Вывод:

false
true
true
true

ArrayDeque наследует equals() от Object, поэтому a.equals(b) истинно только для одного и того же объекта. Чтобы сравнить содержимое, деки превращают в списки или массивы — порядок при этом сохраняется от головы к хвосту. По той же причине ArrayDeque нельзя использовать как ключ HashMap или элемент HashSet в расчёте на сравнение по содержимому. У LinkedList такой проблемы нет: он реализует List и сравнивает элементы.

Потокобезопасность и fail-fast итераторы

ArrayDeque не потокобезопасен: если два потока одновременно добавляют или удаляют элементы, индексы head и tail могут разойтись с содержимым массива, и элементы потеряются. Для общей очереди между потоками используют ConcurrentLinkedDeque или блокирующую LinkedBlockingDeque.

Итераторы ArrayDeque — fail-fast: если изменить деку во время обхода не через iterator.remove(), итератор, как правило, бросит ConcurrentModificationException. Документация подчёркивает, что это проверка «по возможности» (best-effort): исключение не гарантировано, и строить на нём логику программы нельзя. Общий механизм разобран в разделе Возникновение ConcurrentModificationException.

Collections.synchronizedDeque() не существует

У списков и множеств есть обёртки Collections.synchronizedList() и synchronizedSet(), а для Deque такого метода в классе Collections нет. Обёртка Collections.synchronizedCollection(deque) вернёт Collection без методов push() и pollFirst(). Поэтому для многопоточного кода сразу выбирают класс из java.util.concurrent — обзор в разделе Потокобезопасные коллекции Java.

ArrayDeque vs LinkedList vs Stack

Для стека и очереди в однопоточном коде лучше всего подходит ArrayDeque: документация Java прямо говорит, что он, скорее всего, быстрее Stack в роли стека и быстрее LinkedList в роли очереди. LinkedList выбирают, когда нужны null или интерфейс List, а Stack остался только в старом коде.

Признак ArrayDeque LinkedList Stack
Появился Java 6 Java 1.2 Java 1.0
Внутреннее устройство Кольцевой массив Двусвязный список узлов Массив (наследник Vector)
Интерфейсы Deque List и Deque List
Операции на концах O(1), без создания объектов O(1), но на каждое добавление создаётся узел Только вершина; каждый метод synchronized
Доступ по индексу Нет get(i) — O(n) get(i) — O(1), что нарушает принцип стека
null Нет — NullPointerException Да Да
Потокобезопасность Нет Нет Да, ценой блокировки на каждом вызове
Порядок обхода после push(1), push(2), push(3) [3, 2, 1] — от вершины [3, 2, 1] — от вершины [1, 2, 3] — от дна (подробнее)
equals() по содержимому Нет, по ссылке Да Да
Когда выбирать Стек и очередь по умолчанию Нужны null, List или вставка через ListIterator Только поддержка старого кода

Разница в скорости между ArrayDeque и LinkedList при одинаковой оценке O(1) объясняется памятью: элементы массива лежат рядом и хорошо попадают в кэш процессора, а LinkedList выделяет отдельный объект на каждый элемент и нагружает сборщик мусора. Устройство узлов разобрано в уроке LinkedList, история класса Stack — в уроке Legacy Classes.

Когда использовать ArrayDeque

ArrayDeque берут по умолчанию, когда в однопоточном коде нужен стек (отмена действий, обход в глубину, разбор выражений) или очередь FIFO (обход в ширину, буфер задач, скользящее окно последних N значений). Другая реализация нужна, только если требование ниже прямо противоречит его свойствам.

Требование Что взять вместо ArrayDeque
Нужно хранить null LinkedList
Нужен доступ по индексу ArrayList
Элементы извлекаются по приоритету, а не по порядку добавления PriorityQueue
Очередь общая для нескольких потоков ConcurrentLinkedDeque, LinkedBlockingDeque
Нужен жёсткий предел размера ArrayBlockingQueue, LinkedBlockingDeque(capacity)

Часто задаваемые вопросы

Чем ArrayDeque отличается от ArrayList?

Оба хранят элементы в массиве, но ArrayList всегда начинает данные с индекса 0, поэтому add(0, e) и remove(0) сдвигают все элементы — O(n). ArrayDeque хранит индекс головы и выполняет те же операции за O(1). Взамен у него нет get(int) и set(int, E): если нужен доступ по индексу, берут ArrayList.

Уменьшается ли ArrayDeque после удаления элементов?

Нет. Освободившиеся ячейки обнуляются, и сборщик мусора может удалить сами объекты, но массив остаётся прежнего размера; метода вроде trimToSize() у ArrayDeque нет. Чтобы вернуть память после пиковой нагрузки, создают копию: deque = new ArrayDeque<>(deque); — новый массив будет рассчитан на текущее число элементов.

Можно ли ограничить размер ArrayDeque?

Встроенного ограничения нет: число в конструкторе задаёт только начальную ёмкость. Для «последних N элементов» ограничение пишут вручную: if (deque.size() == n) deque.pollFirst(); deque.offerLast(x);. Если нужна очередь, которая сама отказывает в добавлении, берут ArrayBlockingQueue или LinkedBlockingDeque с ёмкостью — пример в разделе Реализации Queue и Deque.

Почему ArrayDeque быстрее LinkedList, если у обоих операции на концах O(1)?

O(1) говорит только о том, что время не зависит от размера, а не о самом времени. LinkedList на каждое добавление создаёт объект-узел, а его узлы разбросаны по памяти. ArrayDeque записывает ссылку в готовую ячейку массива, соседние элементы лежат рядом, а редкое копирование при росте распределяется на все добавления. Сводное сравнение — в разделе ArrayDeque vs LinkedList vs Stack.

Practice

Reinforce this lesson with hands-on tasks, each with a level and a time estimate.

All module tasks →

Video Explanation

Prefer video format? Watch this lesson with examples and explanations.

Comments

Please log in or register to have a possibility to add comment.