W3docs

Интерфейс Queue в Java

Операции FIFO-очереди в Java через интерфейс Queue — offer, poll, peek — и его реализации.

Queue<E> — это Collection<E> плюс понятие головы. Голова — это следующий элемент, который будет удалён; все остальные ждут своей очереди за ним. Семантика по умолчанию — и та, которую использует почти каждая реализация в JDK — FIFO: первым пришёл, первым ушёл. Производители добавляют элементы в хвост через offer; потребители извлекают их из головы через poll. В этой главе рассматривается контракт: шесть методов, два стиля обработки ошибок, правило обработки null и какую реализацию выбирать в той или иной ситуации.

Два способа сделать каждое действие — по замыслу

Каждая операция с очередью существует в двух вариантах: один бросает исключение при неудаче, другой возвращает специальное значение. Javadoc представляет это в виде сетки 3×2:

ОперацияБросает исключениеВозвращает специальное значение
Вставкаboolean add(E e) (бросает IllegalStateException, если очередь заполнена)boolean offer(E e) (возвращает false, если очередь заполнена)
УдалениеE remove() (бросает NoSuchElementException, если очередь пуста)E poll() (возвращает null, если очередь пуста)
ПросмотрE element() (бросает NoSuchElementException, если очередь пуста)E peek() (возвращает null, если очередь пуста)

Столбец «бросает исключение» унаследован от Collection и List — он привычен, но неудобен, когда случай пустой/заполненной очереди является нормой (потребитель ожидает следующей задачи, ограниченная очередь отклоняет лишнего производителя). Столбец «возвращает специальное значение» был добавлен для очередей, чтобы можно было писать циклы, не опираясь на исключения для управления потоком:

String item;
while ((item = queue.poll()) != null) {
  process(item);
}

Используйте форму с возвратом значения по умолчанию. Прибегайте к remove/element только тогда, когда пустая очередь в данный момент является настоящей ошибкой, которую вы хотите обнаружить через исключение.

Null и Queue несовместимы

Поскольку poll() и peek() возвращают null для обозначения «пусто», допускать null в качестве реального элемента — значит создавать почву для неоднозначности. Каждая очередь общего назначения в JDK, за исключением LinkedList, отвергает nullArrayDeque, PriorityQueue, ArrayBlockingQueue, все конкурентные очереди. LinkedList позволяет добавить null, но в этом случае вы нарушаете контракт: больше нет способа отличить «голова равна null» от «очередь пуста».

Правило: не храните null в очереди. Выбирайте ArrayDeque вместо LinkedList, и язык будет обеспечивать это за вас.

FIFO — поведение по умолчанию, но не универсальное

Queue не требует FIFO. Интерфейс определяет голову и операции, извлекающие из неё; то, как определяется голова, зависит от реализации. Две реализации в java.util, которые не являются FIFO:

  • PriorityQueue — головой всегда является наименьший элемент (по естественному порядку или через Comparator). Вставки происходят туда, куда предписывает куча, а не в хвост.
  • Блокирующие варианты в java.util.concurrentPriorityBlockingQueue, DelayQueue и т. д. — упорядочивают по приоритету, дедлайну и пр.

Всё остальное (ArrayDeque, LinkedList, ArrayBlockingQueue, LinkedBlockingQueue, ConcurrentLinkedQueue) работает в режиме FIFO.

Выбор реализации

Для однопоточного случая:

  • ArrayDeque — по умолчанию. Кольцевой массив, без выделения памяти на каждый элемент, быстрый. Используйте как Queue<E> или как Deque<E> в зависимости от того, нужен ли вам доступ с обоих концов.
  • LinkedList — работает, также реализует List. Выбирайте только тогда, когда действительно нужны оба интерфейса на одном объекте. Глава о LinkedList охватывает компромиссы.
  • PriorityQueue — когда нужно «следующим берём наименьший ожидающий элемент», а не FIFO.

Для многопоточного случая (подробно рассматривается в части о конкурентности позже — здесь только указатели):

  • ConcurrentLinkedQueue — неблокирующий FIFO. Неограниченный.
  • ArrayBlockingQueue — ограниченный, на основе массива, блокирует put при заполнении и take при пустой очереди. Классическая очередь производитель/потребитель.
  • LinkedBlockingQueue — опционально ограниченный, вариант на связанных узлах.
  • PriorityBlockingQueue — конкурентный PriorityQueue. Неограниченный.

Большинство прикладного кода использует одну из первых четырёх. Блокирующие очереди — это инструмент для построения пулов воркеров и back-pressure.

Что можно вызвать на любом Queue

Помимо шести методов, специфичных для головы, Queue также является Collection — поэтому size, isEmpty, contains, iterator, forEach, stream, clear, toArray — всё это работает. Порядок итерации — это порядок, в котором элементы извлекались бы через poll для FIFO-реализаций (ArrayDeque, LinkedList), но для PriorityQueue порядок итератора не является порядком по приоритету — он обходит внутренний массив кучи как есть. Если вам нужны элементы в порядке приоритета, придётся опустошить очередь с помощью poll.

Рабочий пример: производитель/потребитель в одном потоке

Программа ниже использует ArrayDeque как FIFO-очередь для моделирования небольшой системы печати: задания поступают со временем (это представлено добавлением пакетами через offer), а воркер опустошает каждый пакет через poll. Пара стилей обработки ошибок показана вверху, чтобы вы могли точно увидеть, когда каждая форма срабатывает.

java— editable, runs on the server

Несколько вещей, которые стоит отметить по результатам выполнения:

  • poll и peek тихо вернули null, когда очередь была пуста; remove и element бросили исключения. Оба поведения являются частью контракта — выбирайте то, которое соответствует тому, является ли «пусто» ошибкой или просто состоянием.
  • offer(null) бросило исключение на ArrayDeque. Это реализация обеспечивает выполнение правила, на которое опирается интерфейс.
  • PriorityQueue извлёк элементы в порядке 10, 20, 30, 40 — отсортированном порядке, а не порядке вставки. Тот же интерфейс Queue, но совершенно другое правило выбора головы.

Что дальше

Первая не-FIFO реализация заслуживает отдельной главы — PriorityQueue — это очередь на основе кучи, где головой всегда является наименьший элемент. Это базовый ингредиент практически любого планировщика «обработай наиболее срочную задачу следующей» в JDK.

Практика

Практика
Внутри цикла вы пишете `while ((job = queue.poll()) != null) { process(job); }`. Если вместо этого использовать `queue.remove()`, что изменится на уровне контракта?
Внутри цикла вы пишете `while ((job = queue.poll()) != null) { process(job); }`. Если вместо этого использовать `queue.remove()`, что изменится на уровне контракта?
Was this page helpful?