Collections.asLifoQueue: примеры (JAVA)

Применение asLifoQueue для LIFO в Java
Раздел: Коллекции (Collection Framework) - Queue/Deque
Collections.asLifoQueue(Deque deque): Queue

Описание и сигнатура

Метод Collections.asLifoQueue предоставляет адаптер, превращающий любой объект, реализующий интерфейс Deque<E>, в представление типа Queue<E> с поведением LIFO (последним пришёл - первым вышел). Возвращаемый объект является видом (view) на переданный Deque, то есть операции над адаптером влияют на исходную структуру и наоборот.

Сигнатура:

public static <T> Queue<T> asLifoQueue(Deque<T> deque)

Аргументы:

  • deque - обязательный параметр типа Deque<T>. Нельзя передавать null. Поддержка null-элементов зависит от конкретной реализации Deque.

Возвращаемое значение:

  • Возвращается объект Queue<T>, который использует методы deque для реализации операций очереди в LIFO-манере: добавление выполняется через добавление в вершину (аналогично addFirst), удаление и просмотр элемента - через доступ к вершине (removeFirst/peekFirst и т.д.).
  • Возвращаемый объект не копирует содержимое deque, это представление поверх него.

Поведение основных методов (соответствие вызовов):

  • add(e) / offer(e) → вставка в начало: аналог deque.addFirst(e) / deque.offerFirst(e).
  • remove() / poll() → удаление из начала: аналог deque.removeFirst() / deque.pollFirst().
  • element() / peek() → просмотр первого элемента: аналог deque.getFirst() / deque.peekFirst().
  • Методы size(), isEmpty(), contains(Object) и т.д. делегируются напрямую на deque.

Особенности и ограничения:

  • Если deque равен null, будет выброшено NullPointerException.
  • Возвращаемая очередь наследует ограничения и семантику исходного Deque: поддержка null, ограничения размера, потокобезопасность зависят от конкретной реализации.
  • Если исходный Deque не поддерживает операцию (например, является неизменяемым), соответствующие вызовы адаптера породят UnsupportedOperationException.

Короткие примеры использования

Пример 1: базовые операции с ArrayDeque

import java.util.*;

public class Example1 {
    public static void main(String[] args) {
        Deque<String> deque = new ArrayDeque<>();
        Queue<String> lifo = Collections.asLifoQueue(deque);

        lifo.add("A");
        lifo.add("B");
        lifo.add("C");

        System.out.println(lifo.poll()); // извлекает C
        System.out.println(lifo.peek()); // показывает B
        System.out.println(lifo.remove()); // извлекает B
        System.out.println(lifo.poll()); // извлекает A
        System.out.println(lifo.poll()); // null
    }
}
C
B
B
A
null

Пример 2: адаптер над LinkedList с проверкой null

import java.util.*;

public class Example2 {
    public static void main(String[] args) {
        Deque<Integer> deque = new LinkedList<>();
        Queue<Integer> lifo = Collections.asLifoQueue(deque);

        lifo.offer(1);
        lifo.offer(2);
        lifo.offer(3);

        // LinkedList допускает null, но адаптер использует методы deque
        deque.addFirst(null);

        System.out.println(lifo.poll()); // null (вершина)
        System.out.println(lifo.poll()); // 3
    }
}
null
3

Пример 3: передача адаптера туда, где требуется Queue

import java.util.*;

public class Example3 {
    static void processQueue(Queue<String> q) {
        q.add("X");
        System.out.println(q.poll());
    }

    public static void main(String[] args) {
        Deque<String> deque = new ArrayDeque<>();
        Queue<String> lifo = Collections.asLifoQueue(deque);
        processQueue(lifo); // работает как LIFO
    }
}
X

Аналоги в Java и их свойства

  • Deque напрямую
  • Интерфейс Deque поддерживает методы push/pop и addFirst/removeFirst. При работе со стековой семантикой предпочтительнее использовать Deque напрямую, так как он предоставляет явные операции стека.

  • ArrayDeque и LinkedList
  • Конкретные реализации ArrayDeque и LinkedList часто используются как стек. ArrayDeque обычно быстрее и более предпочтителен для большинства задач, если не требуется доп. поведение списка.

  • Stack (устаревший)
  • Класс Stack<E> унаследован от Vector и считается устаревшим по сравнению с реализациями на основе Deque. Предпочтение обычно отдают Deque.

  • BlockingDeque
  • Если требуется блокирующая LIFO-очередь, можно использовать LinkedBlockingDeque и его методы putFirst/takeFirst. Обёртка Collections.asLifoQueue вернёт неполноценное представление, которое не содержит блокирующих методов, поэтому в многопоточной блокирующей среде лучше работать с BlockingDeque напрямую.

Эквиваленты в других языках и отличия

  • Python
  • Модуль collections.deque используется как стек: append / pop. Отличие: API прямо поддерживает LIFO; вид-адаптера не нужен.

    from collections import deque
    
    d = deque()
    d.append('A')
    d.append('B')
    print(d.pop())
    print(d.pop())
    B
    A
  • JavaScript
  • Массивы используются как стек через push/pop. Нет отдельного адаптера.

    const a = [];
    a.push('A');
    a.push('B');
    console.log(a.pop());
    console.log(a.pop());
    B
    A
  • C#
  • Класс Stack<T> предоставляет стековую семантику. Также есть ConcurrentStack<T>.

    using System;
    using System.Collections.Generic;
    
    class P{ static void Main(){
        var s = new Stack<string>();
        s.Push("A"); s.Push("B");
        Console.WriteLine(s.Pop());
        Console.WriteLine(s.Pop());
    }}
    B
    A
  • PHP
  • Можно использовать массив с array_push/array_pop или SplStack. Отличие: нет адаптера из двойной очереди.

    $s = [];
    array_push($s, 'A');
    array_push($s, 'B');
    echo array_pop($s) . PHP_EOL;
    echo array_pop($s) . PHP_EOL;
    B
    A
  • Go
  • В стандартной библиотеке стека нет, используют срезы или container/list - push/pop реализуются вручную. Отличие: отсутствие встроенного адаптера.

    package main
    import "fmt"
    func main(){
        s := []string{}
        s = append(s, "A")
        s = append(s, "B")
        fmt.Println(s[len(s)-1])
        s = s[:len(s)-1]
    }
    
    B
  • Kotlin
  • При запуске на JVM используются те же коллекции, что и в Java. Kotlin-коллекция ArrayDeque также доступна и предоставляет методы для работы со стеком. Отличие: Kotlin предлагает более выразительный синтаксис и расширения.

Типичные ошибки и их причины

  • NullPointerException при передаче null
  • Если вызвать Collections.asLifoQueue(null), будет выброшен NullPointerException.

    Queue<String> q = Collections.asLifoQueue(null);
    Exception in thread "main" java.lang.NullPointerException
    	at java.util.Objects.requireNonNull(Objects.java:...) // примерный стек
  • UnsupportedOperationException при попытке изменить неизменяемый deque
  • Если передан неизменяемый или ограниченный по операциям Deque, попытка добавления через адаптер приведёт к UnsupportedOperationException.

    Deque<String> d = Collections.unmodifiableCollection(new ArrayDeque<>());
    Queue<String> q = Collections.asLifoQueue((Deque<String>) d);
    q.add("X");
    Exception in thread "main" java.lang.UnsupportedOperationException
    	at java.util.Collections$UnmodifiableCollection.add(Collections.java:...)
  • Путаница с порядком при итерации
  • Итератор адаптера использует порядок итерации Deque. Если элементы добавлялись в начало, итератор вернёт их от вершины к низу. Непонимание этого может привести к неожиданным результатам при обходе.

  • Проблемы многопоточности
  • Адаптер не добавляет синхронизации. Если требуется потокобезопасность, следует использовать потокобезопасную реализацию Deque (например, ConcurrentLinkedDeque) или блокирующие структуры и работать с ними напрямую.

Изменения в поведении и совместимость

API Collections.asLifoQueue представляет простую адаптацию и в целом не претерпевал существенных изменений. Поведение стабильно: метод возвращает view над переданным Deque и делегирует операции ему. Изменения в релизах Java касались в основном общей библиотеки коллекций и появлению новых реализаций Deque и Concurrent-реализаций, которые можно использовать в сочетании с адаптером.

Замечания по совместимости:

  • Поведение зависит от реализации Deque, поэтому при обновлении JDK могут появиться новые реализации с лучшей производительностью, но семантика адаптера останется прежней.

Расширенные и редкие сценарии использования

1) Потокобезопасный LIFO через ConcurrentLinkedDeque

Пример java
import java.util.*;
import java.util.concurrent.*;

public class Adv1 {
    public static void main(String[] args) throws InterruptedException {
        Deque<String> deque = new ConcurrentLinkedDeque<>();
        Queue<String> lifo = Collections.asLifoQueue(deque);

        Runnable producer = () -> {
            for (int i = 0; i < 3; i++) lifo.add(Thread.currentThread().getName() + "-" + i);
        };

        Thread t1 = new Thread(producer, "P1");
        Thread t2 = new Thread(producer, "P2");
        t1.start(); t2.start();
        t1.join(); t2.join();

        String e;
        while ((e = lifo.poll()) != null) System.out.println(e);
    }
}
P2-2
P2-1
P2-0
P1-2
P1-1
P1-0

Вывод может варьироваться, но порядок извлечения для каждого потока остаётся LIFO относительно его вставок в деку.

2) Использование с BlockingDeque для получения LIFO-очереди с блокировкой (примечание)

Если передать LinkedBlockingDeque в asLifoQueue, получится Queue-view без блокирующих методов. Для блокирующей LIFO-логики требуется работать напрямую с BlockingDeque (например, putFirst/takeFirst).

Пример java
import java.util.concurrent.*;

public class Adv2 {
    public static void main(String[] args) throws InterruptedException {
        LinkedBlockingDeque<String> deque = new LinkedBlockingDeque<>();
        // Лучше использовать методы BlockingDeque напрямую
        deque.putFirst("A");
        System.out.println(deque.takeFirst());
    }
}
A

3) Обёртка адаптера для передачи в API, ожидающее Queue

Некоторые сторонние библиотеки принимают Queue. В таком случае можно передать LIFO-адаптер, чтобы обеспечить стековую семантику без изменения сигнатуры API.

Пример java
// Имеется метод библиотеки: void handle(Queue<String> q)
// Можно передать Collections.asLifoQueue(deque) и внутри handle
// будут использоваться методы очереди, но фактически с LIFO-порядком.
(Зависит от конкретного API, явный вывод отсутствует)

4) Комбинация с synchronized коллекциями

Если требуется простая синхронизация, можно синхронизировать исходный дек: вызов Collections.synchronizedCollection или синхронизировать блоки вокруг обращений. Однако для высоконагруженных сценариев предпочтительнее использовать concurrent-реализации.

Пример java
Deque<String> deque = new ArrayDeque<>();
Queue<String> lifo = Collections.asLifoQueue(deque);

synchronized(deque) {
    lifo.add("X");
}
(Нет прямого вывода, демонстрация синхронизации)

5) Сериализация и совместимость представления

Адаптер сам по себе не сериализируем, даже если исходный Deque реализует Serializable. В случаях, когда требуется сериализация, рекомендуется сериализовать сам Deque или создать копию содержимого в сериализуемой структуре.

Пример java
// Рекомендуется сериализовать сам deque, а не адаптер
(Совет по проектированию, вывода нет)

джава Collections.asLifoQueue function comments

En
Collections.asLifoQueue Returns a view of a Deque as a Last-in-first-out (Lifo) Queue