Вступление
Java Collections Framework - это фундаментальная и важная среда, которую любой сильный Java-разработчик должен знать как свои пять пальцев.
Коллекция в Java определяется как группа или набор отдельных объектов, которые действуют как единый объект.
В Java существует множество классов коллекций, и все они расширяют
интерфейсы java.util.Collection
и java.util.Map
Эти классы в
основном предлагают разные способы составить коллекцию объектов в рамках
одного.
Коллекции Java - это среда, которая обеспечивает множество операций над коллекцией - поиск, сортировку, вставку, манипулирование, удаление и т. Д.
Это четвертая и последняя часть серии статей о Коллекциях Java :
- Интерфейс списка
- Установленный интерфейс
- Интерфейс карты
- Очереди, Deques, Stacks ( вы здесь )
Очередь
Давайте начнем эту заключительную статью серии с интерфейса
java.util.Queue
Принцип
Во-первых, для чего это нужно? Queue
предназначена для хранения
элементов до их
обработки
. Некоторые из них могут иметь фиксированную емкость, что означает, что
они могут содержать только определенное количество элементов.
Итак, идея состоит в том, чтобы поместить некоторые элементы в Queue
,
а затем получить их после этого. Как правило, очереди возвращают
элементы в соответствии с шаблоном First-In First-Out (FIFO) , то есть
сначала возвращается самый старый элемент очереди, затем самый старый и
т. Д.
Вы можете думать о FIFO как о очереди перед магазином. Первый в очереди встает и входит первым.
Но могут быть и другие реализации, которые соблюдают шаблон Last-In
First-Out (LIFO) , или даже отвечают какой-то системе приоритетов
(например, с использованием Comparator
).
Вы можете думать о LIFO как о стопке монет. Последний, который кладут на вершину стопки, снимают первым.
Давайте теперь исследуем особенности интерфейса Queue
Добавление элемента
Начнем с добавления элемента в Queue
. Во- первых, давайте Instantiate
один с помощью ArrayDeque
реализации, который также реализует Deque
интерфейс мы рассмотрим позже:
Queue<Integer> queue = new ArrayDeque<>();
Чтобы добавить элемент в эту Queue
, у нас есть две возможности: метод
add()
или метод offer()
.
Начнем с первого:
queue.add(3);
А с последним:
queue.offer(4);
Оба возвращают boolean
значение, указывающее, был ли элемент добавлен
в Queue
или нет, в зависимости от его емкости (если это применимо). В
чем же тогда разница между обоими методами?
Что ж, первый на самом деле никогда не вернет false
, а скорее
выбрасывает Exception
при добавлении элемента в полную Queue
. С
другой стороны, второй в таких случаях false
Вместо ArrayDeque
, который не ограничен, давайте использовать
LinkedBlockingQueue
которому можно назначить емкость:
Queue<Integer> queue = new LinkedBlockingQueue<>(1);
Здесь мы создали очередь, которая может одновременно содержать не более
одного элемента. Следовательно, мы не можем использовать метод add()
два раза подряд без исключения:
queue.add(3);
queue.add(4);
Попытка добавить эти два элемента приведет к:
java.lang.IllegalStateException: Queue full
at java.base/java.util.AbstractQueue.add(AbstractQueue.java:98)
С другой стороны, использование offer()
ничего не даст и в результате
false
Получение элемента
Как указывалось ранее, Queue
обычно уважает FIFO, что означает, что
она сначала вернет первый введенный элемент, если мы его извлекаем.
Интерфейс предлагает несколько методов для получения элементов. Два из
них, remove()
и poll()
, удаляют элемент перед его возвратом. Два
других element()
и peek()
просто возвращают его, но не удаляют.
remove()
и element()
вызовут исключение при вызове в пустой Queue
:
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(3);
queue.offer(4);
queue.poll();
queue.peek();
Здесь мы соберем элементы 3
и 4
, но в первый раз элемент будет
удален (через poll()
), а во второй раз нет (через peek()
), оставив
нашу очередь с элементом 4
в нем.
Использование remove()
и element()
вместо poll()
и peek()
соответственно дало бы те же результаты, поскольку в нашем случае
очередь никогда не бывает пустой.
Итерация по элементам
Помимо индексированных циклов while
и for
, интерфейс Queue
Iterable
и предоставляет Iterator
, что делает его подходящим для
цикла for-each
:
for (Integer element: queue) {
System.out.println(element);
}
Этот цикл выводит на консоль каждый элемент очереди.
Начиная с Java 8, конечно, есть возможность вызвать метод forEach()
,
передав ссылку на метод :
queue.forEach(System.out::println);
Это дает тот же результат, что и в предыдущем цикле.
Если вы хотите узнать больше об итерируемом интерфейсе в Java , мы вам поможем!
Реализации
Теперь, какие классы реализуют интерфейс Queue
Существует несколько
реализаций интерфейса, но на самом деле они наиболее актуальны:
LinkedList
: Хотя этот класс в основном известен какList
, он также реализует интерфейсQueue
Эта реализация работает, связывая свои элементы вместе и проходя эту цепочку при итерации или поиске элементов.ArrayDeque
: реализация какQueue
иDeque
. Он поддерживается массивом, который может быть увеличен, когда количество элементов превышает его текущую емкость.DelayQueue
: может содержать только элементы, реализующиеDelayed
- элементы, которые становятся активными через определенное время.DelayQueue
будет доставлять только те элементы, чьи задержки истекли.PriorityQueue
: упорядочивает элементы в соответствии с их естественным порядком илиComparator
(если предоставляется). Это означает, что он не работает с использованием принципа FIFO, а скорее возвращает элемент с наивысшим приоритетом (определяемый тем, как они сравниваются друг с другом).
Представим себе систему аномалий с enum
определяющим их серьезность:
public class Anomaly implements Comparable<Anomaly> {
private String log;
private Severity severity;
public Anomaly(String log, Severity severity) {
this.log = log;
this.severity = severity;
}
@Override
public int compareTo(Anomaly o) {
return severity.compareTo(o.severity);
}
private enum Severity {
HIGH,
MEDIUM,
LOW
}
}
Здесь аномалии естественным образом упорядочены по степени серьезности
(поскольку enum
естественным образом упорядочены по порядку их
объявления).
Итак, если бы мы добавили две аномалии в PriorityQueue
без
Comparator
, одну LOW
и одну HIGH
, тогда метод poll()
сначала
вернул бы вторую аномалию, а затем первую:
Queue<Anomaly> anomalies = new PriorityQueue<>();
Anomaly optionalInformationNotRetrievedAnomaly = new Anomaly("Couldn't retrieve optional information", Anomaly.Severity.LOW);
anomalies.offer(optionalInformationNotRetrievedAnomaly);
Anomaly databaseNotReachableAnomaly = new Anomaly("Couldn't contact database", Anomaly.Severity.HIGH);
anomalies.offer(databaseNotReachableAnomaly);
anomalies.poll(); // This would return 'databaseNotReachableAnomaly'
Теперь, если мы передадим Comparator
PriorityQueue
, скажем, тот,
который меняет естественный порядок на противоположный:
Queue<Anomaly> anomalies = new PriorityQueue<>(Comparator.reverseOrder());
Затем в том же сценарии, что и раньше, метод poll()
вернет первую
аномалию - это optionalInformationNotRetrievedAnomaly
.
Deque
Теперь, когда мы рассмотрели Queue
, перейдем к Deque
.
Принцип
Deque означает Double Ended
Queue , что означает,
что это очередь, к которой могут получить доступ оба конца, и поэтому ее
можно использовать как со стилями FIFO, так и LIFO. По умолчанию он
организует свой элемент в стиле LIFO, что означает, что получение
первого в Deque
вернет последний добавленный.
Добавление элемента
Перейдем к Deque
со вставкой элементов. Для этого есть несколько
возможностей:
- Некоторые методы добавляют элемент вверху, некоторые - внизу
- Некоторые методы выдают исключение, если
Deque
заполнена, некоторые - нет.
Сведем их в таблицу:
Вершина Нижний
Без исключения offerFirst()
offer()
, offerLast()
Исключение addFirst()
, push()
add()
, addLast()
Допустим, у нас есть Deque
of Integer
и мы вызываем addFirst()
с
целыми числами 3
и 4
:
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(3);
deque.addFirst(4);
Затем двухсторонняя очередь будет содержать 4
и 3
в указанном
порядке.
Если бы мы использовали addLast()
, то он содержал бы 3
и 4
в этом
порядке. То же самое произошло бы с offerFirst()
и offerLast()
соответственно.
Получение и удаление элемента
Теперь давайте посмотрим, как получить элементы из Deque
. Опять же,
есть несколько возможностей:
- Некоторые методы возвращают первый элемент, некоторые - последний.
- Некоторые методы удаляют элемент при возврате, некоторые нет.
- Некоторые методы вызывают исключение, если
Deque
пуста, некоторые - нет.
Чтобы упростить задачу, мы также обобщим это в таблице:
Первый (верхний) элемент, без удаления Первый (верхний) элемент, удаление
Без исключения peek()
, peekFirst()
poll()
, pollFirst()
Исключение getFirst()
, element()
remove()
, removeFirst()
, pop()
Последний (нижний) элемент, без удаления Последний (нижний) элемент, удаление
Без исключения peekLast()
pollLast()
Исключение getLast()
removeLast()
Допустим, у нас есть Deque
of Integer
с элементами 4
и 3
сверху
вниз. И мы вызываем peekFirst()
:
Deque<Integer> deque = new ArrayDeque<>();
deque.push(3);
deque.push(4);
deque.peekFirst();
Тогда это вернет 4
без удаления элемента. Если бы мы использовали
peekLast()
, то вернули бы 3
.
Теперь, если бы мы использовали removeFirst()
или pop()
, мы
получили бы 4
но в конце Deque
содержал бы только 3
Итерация по элементам
Что касается Queue
, мы можем выполнять итерацию, используя
стандартные механизмы и метод forEach()
. Нам просто нужно помнить,
что по умолчанию Deque
организует свои элементы в стиле LIFO и,
следовательно, будет перебирать их сверху вниз:
Deque<Integer> deque = new ArrayDeque<>();
deque.push(3);
deque.push(4);
deque.forEach(System.out::println);
Это напечатает:
4
3
Вы также можете использовать Iterator
:
Deque<Integer> deque = new ArrayDeque<>();
deque.push(3);
deque.push(4);
for (Iterator<Integer> iterator = deque.iterator(); iterator.hasNext();) {
System.out.println(iterator.next());
}
Это также напечатает:
4
3
Реализации
ArrayDeque
: это тот, который мы использовали дляQueue
и который поддерживаетсяarray
. Реализует какQueue
иDeque
.LinkedList
: реализует какQueue
,Deque
иList
. Мы также видели это раньше.LinkedBlockingDeque
: он немного похож наLinkedList
, но может быть ограничен. Таким образом, операции вставки, которые мы видели ранее, вызовут исключение, если этаDeque
будет заполнена.
Куча?
Стоит отметить, что Stack
тоже существует. Он был представлен в начале
Java и должен был использоваться как коллекция LIFO с методами push()
и pop()
Почему бы тогда не использовать это?
Потому что документация советует нам использовать Deque
который
предлагает более согласованный API. Кроме того, Stack
является
подклассом Vector
и поэтому тесно связан с ним, что делает его List
, который концептуально отличается от стека.
Заключение
Java Collections Framework - это фундаментальная структура, которую каждый Java-разработчик должен знать, как использовать.
В этой статье мы поговорили об Queue
и Deque
и рассмотрели их
основные операции. Полный код этой статьи можно найти на
GitHub .