Kolejki w Javie - jak wybrać właściwą implementację?

Drewniane figurki ludzi w kolejce, symbolizujące queue java. Jak zaimplementować FIFO? Odcinek #007.

Napisano przez

Tymoteusz Sobczak

Opublikowano

18 paź 2026

Spis treści

Gdy zadania mają być obsługiwane w kolejności napływania, zwykła lista szybko przestaje wystarczać. W Javie służy do tego kolejka, czyli struktura, która pozwala bezpiecznie dodawać elementy, pobierać je i sprawdzać pierwszy element bez ręcznego przesuwania danych. Pokażę, jak działa interfejs Queue, kiedy wybrać ArrayDeque, PriorityQueue lub kolejkę współbieżną, a także jak napisać własną prostą implementację.

Najważniejsze informacje o kolejkach w Javie

  • FIFO oznacza, że pierwszy dodany element jest zwykle obsługiwany jako pierwszy.
  • ArrayDeque to najczęściej najlepszy wybór dla zwykłej kolejki działającej w jednym wątku.
  • offer(), poll() i peek() pozwalają obsługiwać brak miejsca lub pustą kolejkę bez wyjątków.
  • PriorityQueue nie działa jak klasyczne FIFO, ponieważ zwraca element o najwyższym priorytecie.
  • BlockingQueue sprawdza się przy komunikacji producentów i konsumentów w wielu wątkach.

Schematyczny obraz struktury danych typu queue w Javie. Dane wchodzą z lewej (Last in, Last out) i wychodzą z prawej (First in, First out).

Jak działa kolejka w Javie

Klasyczna kolejka działa według zasady FIFO, czyli first in, first out. Element trafia na koniec, a jest pobierany z początku. Dobrym obrazowym przykładem jest kolejka żądań HTTP, zadań do wydruku albo wiadomości czekających na obsłużenie przez pracownika.

W Javie kolejka jest opisana przez interfejs Queue z pakietu java.util. Sam interfejs nie przechowuje danych. Określa tylko operacje, a konkretne zachowanie zależy od wybranej implementacji. To rozdzielenie interfejsu od klasy jest praktyczne, bo mogę zmienić sposób przechowywania elementów bez przepisywania kodu, który z kolejki korzysta.

Queue zadania = new ArrayDeque<>();

zadania.offer("Raport");
zadania.offer("Powiadomienie");
zadania.offer("Eksport danych");

System.out.println(zadania.poll()); // Raport
System.out.println(zadania.peek()); // Powiadomienie

Najważniejsze metody mają pary o podobnym zastosowaniu, ale różnym zachowaniu w sytuacji problemowej. Ja w codziennym kodzie zwykle wybieram wersje offer(), poll() i peek(), ponieważ zwracają wartość sygnalizującą brak możliwości wykonania operacji zamiast od razu rzucać wyjątek.

Cel Metoda łagodna Metoda wyjątkowa Rezultat problemu
Dodanie elementu offer(e) add(e) false lub wyjątek
Pobranie i usunięcie początku poll() remove() null lub wyjątek
Sprawdzenie początku peek() element() null lub wyjątek

W kolejce o nieograniczonym rozmiarze offer() najczęściej zwróci true, ale przy strukturach o stałej pojemności może zwrócić false. Trzeba też uważać na wartość null. Wiele implementacji jej nie dopuszcza, a metody takie jak poll() używają null do oznaczenia pustej kolejki.

ArrayDeque, LinkedList czy PriorityQueue

Wybór klasy ma większe znaczenie niż sama znajomość metod. Dla klasycznej kolejki FIFO najczęściej zaczynam od ArrayDeque. Jest oparta na tablicy o zmiennym rozmiarze, dobrze wykorzystuje pamięć i nie wymaga tworzenia osobnego obiektu dla każdego elementu.

LinkedList również implementuje Queue, ale w nowych projektach rzadko jest moim pierwszym wyborem. Lista wiązana przechowuje dodatkowe referencje w węzłach, przez co ma większy narzut pamięci. Jej zaletą jest elastyczność, jednak dla zwykłego dodawania na końcu i usuwania z początku ArrayDeque zazwyczaj jest prostsze i wydajniejsze.

PriorityQueue służy do innego problemu. Nie zwraca najstarszego elementu, lecz ten, który ma najmniejszą wartość według naturalnego porządku albo przekazanego komparatora. To dobre rozwiązanie dla harmonogramu zadań, algorytmu Dijkstry czy systemu, w którym pilniejsze zgłoszenia powinny być obsłużone wcześniej.

Implementacja Najlepsze zastosowanie Najważniejsze ograniczenie
ArrayDeque Klasyczna kolejka FIFO lub deque w jednym wątku Nie jest bezpieczna dla wielu wątków i nie przyjmuje null
LinkedList Proste przykłady i sytuacje, w których potrzebne są także operacje listy Większy narzut pamięci oraz słabsza lokalność danych
PriorityQueue Obsługa według priorytetu Nie zachowuje klasycznej kolejności FIFO
ConcurrentLinkedQueue Nieblokująca kolejka współdzielona przez wątki Nie oferuje oczekiwania na element ani stałego limitu pojemności

Praktyczna zasada jest prosta. Jeśli nie potrzebuję priorytetów ani synchronizacji, deklaruję zmienną jako Queue, a tworzę ArrayDeque. Dzięki temu kod pokazuje zamiar, a implementację można później wymienić bez zmiany całej logiki.

Queue liczby = new ArrayDeque<>();

Praktyczne operacje na elementach kolejki

Najczęstszy scenariusz wygląda tak, że program dodaje zadania, a osobny fragment kodu pobiera je aż do opróżnienia struktury. Do sprawdzania końca używam isEmpty(), a nie porównania rozmiaru z konkretną liczbą. Taki zapis jest czytelniejszy i nie zależy od tego, ile elementów znajdowało się w kolejce na początku.

Queue kolejka = new ArrayDeque<>();

kolejka.offer("plik-1.csv");
kolejka.offer("plik-2.csv");
kolejka.offer("plik-3.csv");

while (!kolejka.isEmpty()) {
    String plik = kolejka.poll();
    System.out.println("Przetwarzam " + plik);
}

Jeżeli chcę tylko zobaczyć pierwszy element, wybieram peek(), bo nie usuwa on danych. Metoda poll() usuwa początek, dlatego wywołanie jej w warunku diagnostycznym może niepostrzeżenie zmienić stan programu. Ten błąd zdarza się często przy logowaniu i testowaniu.

Przeczytaj również: C# LINQ Aggregate - Kiedy używać, a kiedy unikać?

Przykład z własnym porządkiem

Priorytet można zdefiniować przez komparator. W poniższym przykładzie mniejsza liczba oznacza pilniejsze zadanie. Kolejność dodawania nie ma tu decydującego znaczenia.

Queue zadania = new PriorityQueue<>(
    Comparator.comparingInt(Zadanie::priorytet)
);

zadania.offer(new Zadanie("Kopia zapasowa", 3));
zadania.offer(new Zadanie("Awaria serwera", 1));
zadania.offer(new Zadanie("Raport tygodniowy", 5));

Zadanie pilne = zadania.poll();

Trzeba pamiętać, że PriorityQueue gwarantuje właściwy element na początku, ale nie obiecuje pełnego posortowania wszystkich elementów podczas iteracji. Jeśli potrzebuję kompletnej kolejności, pobieram elementy metodą poll() albo stosuję inną strukturę.

Kolejki w aplikacjach wielowątkowych

Zwykłe ArrayDeque nie powinno być współdzielone przez wiele wątków bez dodatkowej synchronizacji. Gdy jeden wątek dodaje zadania, a drugi je pobiera, wybieram klasę z pakietu java.util.concurrent. To nie tylko kwestia wydajności, ale przede wszystkim poprawności operacji wykonywanych równocześnie.

BlockingQueue pasuje do modelu producent-konsument. Producent dodaje dane, a konsument może poczekać, aż pojawi się element. Metoda put() może zatrzymać wątek przy pełnej kolejce, natomiast take() czeka przy pustej. Właśnie takie zachowanie jest przydatne przy przetwarzaniu plików, zadań asynchronicznych i komunikacji między etapami aplikacji.

BlockingQueue zadania = new ArrayBlockingQueue<>(100);

zadania.put("wysyłka-maila");

String zadanie = zadania.take();
System.out.println(zadanie);

Limit 100 elementów chroni tu aplikację przed niekontrolowanym wzrostem pamięci. Jeśli producent działa szybciej niż konsument, kolejka w końcu się zapełni, a system dostanie szansę zwolnić zamiast przyjmować dane bez końca.

Gdy nie chcę blokować wątku, mogę użyć offer() i sprawdzić wynik. ConcurrentLinkedQueue jest przydatna w prostszych scenariuszach współbieżnych, ale nie rozwiązuje problemu oczekiwania na dane. Wybór zależy więc od tego, czy pustka lub pełność kolejki ma zatrzymać pracę, czy tylko zwrócić informację o braku możliwości operacji.

Jak napisać własną kolejkę dla nauki

W projekcie produkcyjnym korzystam z gotowych klas biblioteki standardowej. Własna implementacja ma jednak dużą wartość edukacyjną, bo pokazuje, co naprawdę dzieje się podczas dodawania i usuwania elementów. Najprościej użyć tablicy cyklicznej, w której indeks początku i końca przesuwa się po osiągnięciu ostatniej komórki.

public class SimpleQueue {
    private final Object[] data;
    private int head = 0;
    private int tail = 0;
    private int size = 0;

    public SimpleQueue(int capacity) {
        if (capacity <= 0) {
            throw new IllegalArgumentException("Pojemność musi być dodatnia");
        }
        data = new Object[capacity];
    }

    public boolean offer(E element) {
        if (element == null || size == data.length) {
            return false;
        }

        data[tail] = element;
        tail = (tail + 1) % data.length;
        size++;
        return true;
    }

    @SuppressWarnings("unchecked")
    public E poll() {
        if (size == 0) {
            return null;
        }

        E element = (E) data[head];
        data[head] = null;
        head = (head + 1) % data.length;
        size--;
        return element;
    }

    public boolean isEmpty() {
        return size == 0;
    }
}

Ważny szczegół to wyzerowanie komórki po pobraniu elementu. Dzięki temu obiekt może zostać usunięty przez garbage collector, gdy nie ma już innych referencji. Pominięcie tej instrukcji prowadzi do niepotrzebnego przetrzymywania obiektów w pamięci, nawet jeśli logicznie kolejka jest pusta.

Ta wersja ma stałą pojemność i nie jest bezpieczna wielowątkowo. Nie obsługuje też wszystkich metod interfejsu Queue, dlatego traktowałbym ją jako ćwiczenie z indeksami, buforem cyklicznym i kontrolą stanu, a nie zamiennik ArrayDeque.

Błędy, które najczęściej psują działanie kolejki

  • Użycie PriorityQueue tam, gdzie potrzebne jest FIFO. Priorytet zmienia kolejność obsługi.
  • Wywołanie remove() lub element() bez sprawdzenia, czy kolejka zawiera dane.
  • Współdzielenie ArrayDeque między wątkami bez synchronizacji.
  • Założenie, że iteracja po PriorityQueue zwróci elementy w pełni posortowanej kolejności.
  • Brak limitu w kolejce zasilanej szybciej, niż jest opróżniana.
  • Wstawianie null i późniejsze traktowanie go jako sygnału pustej kolejki.

Ja zaczynam od określenia trzech rzeczy: czy kolejność ma być FIFO, czy elementy mają mieć priorytety oraz czy dane będą współdzielone przez wątki. Te trzy odpowiedzi zwykle prowadzą do właściwej klasy szybciej niż porównywanie wszystkich dostępnych implementacji.

Warto też testować sytuacje brzegowe. Sprawdzam pustą kolejkę, pojedynczy element, przepełnienie bufora oraz kolejność po wielokrotnym zawinięciu indeksu. W przypadku systemów współbieżnych dodaję testy obciążeniowe, bo błąd może pojawiać się tylko przy określonej kolejności działań wątków.

Dobry wybór zależy od sposobu obsługi danych

Do zwykłego bufora zadań wybrałbym ArrayDeque. Gdy ważność zadania ma pierwszeństwo przed momentem dodania, lepsza będzie PriorityQueue. Przy komunikacji między wątkami potrzebuję jednej z implementacji BlockingQueue albo kolejki współbieżnej, zależnie od tego, czy konsument ma czekać na dane.

Najważniejsze jest nie samo zapamiętanie nazw klas, lecz rozpoznanie modelu pracy. Kolejka jest dobrym narzędziem wtedy, gdy dane można obsługiwać etapami, kontrolować tempo przetwarzania i jasno określić, który element powinien zostać pobrany jako następny.

Gdy opanujesz różnicę między offer(), poll() i peek(), a potem dopasujesz implementację do FIFO, priorytetu lub współbieżności, większość problemów z kolejkami w Javie przestaje być zgadywaniem. Zostaje świadomy wybór struktury, jej pojemności i zasad obsługi błędów.

FAQ - Najczęstsze pytania

ArrayDeque jest zwykle lepszym wyborem dla klasycznej kolejki FIFO działającej w jednym wątku. Ma mniejszy narzut pamięci, ponieważ nie tworzy osobnego węzła dla każdego elementu, i zazwyczaj oferuje lepszą lokalność danych. LinkedList warto rozważyć głównie wtedy, gdy potrzebne są także operacje typowe dla listy.

PriorityQueue zwraca element o najwyższym priorytecie, a niekoniecznie ten dodany jako pierwszy. Priorytet może wynikać z naturalnego porządku elementów albo z komparatora, na przykład mniejsza liczba może oznaczać pilniejsze zadanie. Iteracja po PriorityQueue nie gwarantuje pełnego sortowania, dlatego uporządkowane elementy należy pobierać metodą poll().

Do modelu producent-konsument pasuje BlockingQueue, na przykład ArrayBlockingQueue o pojemności 100 elementów. Metoda put() może czekać, gdy kolejka jest pełna, a take() czeka, gdy jest pusta. Limit pojemności ogranicza niekontrolowany wzrost pamięci, gdy producent działa szybciej niż konsument.

ConcurrentLinkedQueue sprawdza się jako nieblokująca kolejka współdzielona przez wątki, gdy wystarczy otrzymać informację o braku elementu. Nie zapewnia jednak oczekiwania na dane ani stałego limitu pojemności. Jeśli konsument ma czekać na element lub trzeba kontrolować rozmiar bufora, lepsza będzie odpowiednia implementacja BlockingQueue.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

java kolejki arraydeque priorityqueue wielowątkowość

Udostępnij artykuł

Tymoteusz Sobczak

Tymoteusz Sobczak

Nazywam się Tymoteusz Sobczak i od 7 lat zajmuję się programowaniem webowym. Moje zainteresowanie tą dziedziną zaczęło się od prostych projektów, które realizowałem w wolnym czasie. Z czasem odkryłem, jak fascynujące jest tworzenie aplikacji, które mogą ułatwiać życie innym. Chętnie dzielę się swoją wiedzą, pomagając czytelnikom zrozumieć złożone zagadnienia związane z programowaniem, od podstawowych technik po bardziej zaawansowane rozwiązania. Pisząc na temat programowania, staram się dostarczać informacje, które są nie tylko użyteczne, ale także zrozumiałe. Zawsze dokładam starań, aby moje źródła były wiarygodne, a treści aktualne. Lubię upraszczać trudne tematy i organizować wiedzę w sposób, który ułatwia naukę. Wierzę, że każdy, kto ma chęci, może stać się dobrym programistą, a ja jestem tutaj, aby wskazać drogę.

Napisz komentarz