Интерфейс 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, отвергает null — ArrayDeque, PriorityQueue, ArrayBlockingQueue, все конкурентные очереди. LinkedList позволяет добавить null, но в этом случае вы нарушаете контракт: больше нет способа отличить «голова равна null» от «очередь пуста».
Правило: не храните null в очереди. Выбирайте ArrayDeque вместо LinkedList, и язык будет обеспечивать это за вас.
FIFO — поведение по умолчанию, но не универсальное
Queue не требует FIFO. Интерфейс определяет голову и операции, извлекающие из неё; то, как определяется голова, зависит от реализации. Две реализации в java.util, которые не являются FIFO:
PriorityQueue— головой всегда является наименьший элемент (по естественному порядку или черезComparator). Вставки происходят туда, куда предписывает куча, а не в хвост.- Блокирующие варианты в
java.util.concurrent—PriorityBlockingQueue,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. Пара стилей обработки ошибок показана вверху, чтобы вы могли точно увидеть, когда каждая форма срабатывает.
Несколько вещей, которые стоит отметить по результатам выполнения:
pollиpeekтихо вернулиnull, когда очередь была пуста;removeиelementбросили исключения. Оба поведения являются частью контракта — выбирайте то, которое соответствует тому, является ли «пусто» ошибкой или просто состоянием.offer(null)бросило исключение наArrayDeque. Это реализация обеспечивает выполнение правила, на которое опирается интерфейс.PriorityQueueизвлёк элементы в порядке10, 20, 30, 40— отсортированном порядке, а не порядке вставки. Тот же интерфейсQueue, но совершенно другое правило выбора головы.
Что дальше
Первая не-FIFO реализация заслуживает отдельной главы — PriorityQueue — это очередь на основе кучи, где головой всегда является наименьший элемент. Это базовый ингредиент практически любого планировщика «обработай наиболее срочную задачу следующей» в JDK.