Java Deque Interface
Операции двусторонней очереди в Java с интерфейсом Deque — addFirst, addLast, pollFirst, pollLast.
Deque<E> — произносится «дек», сокращение от double-ended queue (двусторонняя очередь) — это Queue<E> с двумя концами. В отличие от Queue, которая позволяет извлекать элементы только с головы, Deque даёт возможность добавлять, удалять и просматривать элементы с обоих концов — как с головы, так и с хвоста. Этого единственного дополнения достаточно, чтобы Deque стал основой двух совершенно разных абстракций: он одновременно является очередью и стеком, и JDK рекомендует его в качестве реализации по умолчанию для обоих случаев.
Два конца × три операции × два стиля обработки ошибок
Интерфейс поначалу кажется громоздким, потому что каждая операция представлена в двенадцати вариантах — голова или хвост, вставка или удаление или просмотр, генерация исключения или возврат специального значения. Но это просто сетка Queue 3×2, растянутая на два конца:
| Операция | Первый (голова) — исключение | Первый (голова) — спец. значение | Последний (хвост) — исключение | Последний (хвост) — спец. значение |
|---|---|---|---|---|
| Вставка | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
| Удаление | removeFirst() | pollFirst() | removeLast() | pollLast() |
| Просмотр | getFirst() | peekFirst() | getLast() | peekLast() |
Колонка «исключение» используется тогда, когда пустой или переполненный дек в данной точке является ошибкой. Колонка «спец. значение» — когда пустота является ожидаемым состоянием и вы хотите проверять её в условии цикла.
Отображение на очередь
Deque расширяет Queue, поэтому Deque является Queue. Унаследованные методы — это псевдонимы для вариантов с явным указанием конца:
Метод Queue | Эквивалент Deque |
|---|---|
add(e) / offer(e) | addLast(e) / offerLast(e) |
remove() / poll() | removeFirst() / pollFirst() |
element() / peek() | getFirst() / peekFirst() |
Вставка с хвоста, удаление с головы — это дисциплина FIFO, которую Queue уже обеспечивает, просто выраженная с именами, явно указывающими конец.
Отображение на стек
Deque также определяет три «стековых» метода, которые все работают с головой:
| Метод стека | Эквивалент Deque |
|---|---|
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
Положить сверху, снять сверху — дисциплина LIFO, тот же конец (голова), но противоположный по отношению к вставке/удалению в отображении очереди. Именно поэтому глава о Stack направила вас сюда: современный способ написать стек в Java — Deque<E> stack = new ArrayDeque<>(); и вызывать push / pop / peek.
Правило: null не допускается
Как и Queue, контракт Deque резервирует null в качестве «пустого» сентинела для pollFirst, pollLast, peekFirst, peekLast. Поэтому каждый универсальный Deque в JDK отвергает элементы null с NullPointerException при вставке. Единственное исключение — LinkedList, который допускает null по историческим причинам — и наследует всю двусмысленность, связанную с этим. Не делайте так.
Прямая и обратная итерация
По соглашению Deque имеет два итератора:
iterator()обходит элементы от головы к хвосту — в порядке, в котором вы бы последовательно вызывалиpollFirst.descendingIterator()обходит элементы от хвоста к голове — в порядке, в котором вы бы последовательно вызывалиpollLast.
Оба, как правило, являются fail-fast: изменение дека снаружи итератора во время обхода вызывает ConcurrentModificationException. Используйте Iterator.remove() или removeIf, если нужно изменять дек в ходе обхода.
Выбор реализации
В однопоточном коде есть фактически два варианта:
ArrayDeque— вариант по умолчанию. Кольцевой массив, O(1) на обоих концах, без выделения памяти для отдельных узлов, кеш-дружественный. Отвергаетnull. РеализуетDequeиQueue.LinkedList— узлы двусвязного списка. Также реализуетList, поэтому каждый элемент получает узел с указателямиprev/next. Медленнее на обоих концах, чемArrayDeque, и использует больше памяти. Выбирайте его только если вам действительно нужна семантика иDeque, иListна одном объекте.
Для конкурентного кода (рассматривается позднее в разделе о конкурентности):
ConcurrentLinkedDeque— неблокирующий, неограниченный.LinkedBlockingDeque— опционально ограниченный, блокирующий — вариант для построения очереди с похищением работ или модели производитель/потребитель с противодавлением на любом из концов.
Практический пример: очередь, стек и проверка палиндрома в одном деке
Программа ниже использует один ArrayDeque как FIFO-очередь, другой — как LIFO-стек, а третий — для проверки, является ли предложение палиндромом, сравнивая элементы с обоих концов. Суть в том, что один и тот же интерфейс, и даже один и тот же класс, выполняет все три роли.
Что показывает этот запуск:
- Один и тот же класс выполняет роли как очереди, так и стека, без переименования единственного метода — меняется только то, с какого конца вы обращаетесь.
- Методы «возврата специального значения» тихо возвращают
nullдля пустого дека; методы «генерации исключения» вызываютNoSuchElementException. Выбирайте в зависимости от того, является ли пустота ожидаемым состоянием или ошибкой. - Проверка палиндрома использует оба конца в одном цикле —
pollFirstиpollLastвместе. Это паттерн доступа, который толькоDequeпредоставляет эффективно.
Что дальше
Глава о Deque завершает рассмотрение очередной части фреймворка. Другая важная абстракция в Collection — та, что отвергает дубликаты: Set — это Collection со встроенным контрактом уникальности. Его мы рассмотрим следующим.