Hierarchia kolekcji w Javie decyduje o tym, czy dane będą przechowywane jako uporządkowana lista, zbiór unikalnych elementów, kolejka czy mapa klucz-wartość. Ta java collections hierarchy nie jest teorią do wkuwania na pamięć; w praktyce pomaga dobrać strukturę, która będzie czytelna, szybka i odporna na typowe błędy w kodzie.
Najkrócej rzecz ujmując, chodzi o wybór właściwego kontraktu, a nie przypadkowej klasy
- `Collection` i `Map` tworzą dwie oddzielne gałęzie frameworka.
- `List` zachowuje kolejność i dopuszcza duplikaty, `Set` pilnuje unikalności, a `Queue` i `Deque` obsługują kolejność przetwarzania.
- `Map` przechowuje pary klucz-wartość i nie jest podtypem `Collection`.
- W nowszej Javie, od JDK 21, pojawiły się interfejsy `SequencedCollection`, `SequencedSet` i `SequencedMap`.
- Najlepsza implementacja zależy od dominującej operacji: odczytu po indeksie, wyszukiwania po kluczu, sortowania albo pracy na końcach struktury.
Zanim wejdę w szczegóły, rozbijmy ten temat na dwie główne gałęzie: kolekcje elementów oraz mapy klucz-wartość. To od tego podziału zaczyna się sensowne czytanie całego frameworka.
Jak zbudowana jest hierarchia kolekcji w Javie
W Javie nie ma jednego wielkiego „kosza na dane”. Framework kolekcji jest zorganizowany warstwowo, a podstawowe rozgałęzienie wygląda tak: jedna strona opiera się na `Collection`, druga na `Map`. To ważne, bo `Map` nie jest kolekcją w sensie interfejsu nadrzędnego, tylko osobnym filarem tego samego frameworka.
Ja lubię tłumaczyć to bardzo prosto: jeśli przechowujesz elementy, patrzysz na `Collection` i jej podtypy; jeśli przechowujesz pary, patrzysz na `Map`. Ta różnica brzmi banalnie, ale później decyduje o tym, czy kod będzie naturalny, czy będzie walczył z własnym modelem danych.
| Gałąź | Co przechowuje | Duplikaty | Porządek | Typowe implementacje |
|---|---|---|---|---|
| `Collection` | Elementy | Zależy od podtypu | Zależy od podtypu | `ArrayList`, `HashSet`, `ArrayDeque` |
| `List` | Elementy z indeksami | Tak | Tak | `ArrayList`, `LinkedList`, `CopyOnWriteArrayList` |
| `Set` | Unikalne elementy | Nie | Zależy od implementacji | `HashSet`, `LinkedHashSet`, `TreeSet` |
| `Queue` / `Deque` | Elementy do przetwarzania w kolejności | Zależy od implementacji | Tak, często FIFO albo dwukierunkowy | `ArrayDeque`, `PriorityQueue`, `LinkedList` |
| `Map` | Klucze i wartości | Unikalne klucze | Zależy od implementacji | `HashMap`, `LinkedHashMap`, `TreeMap` |
W nowocześniejszych wersjach Javy do tej układanki doszły też interfejsy sekwencyjne. Jeśli pracujesz na JDK 21 lub nowszym, zobaczysz `SequencedCollection`, `SequencedSet` i `SequencedMap`, czyli ujednolicony sposób pracy z kolekcjami, które mają ustalony porządek przejścia. To nie zmienia podstawowego podziału, ale porządkuje API tam, gdzie kolejność naprawdę ma znaczenie.
Gdy ten szkielet jest już jasny, łatwiej odróżnić listę od zbioru i zrozumieć, dlaczego kolejka ma własne reguły działania.
List, Set i Queue zachowują się inaczej niż sugeruje sam diagram
Na diagramie wszystko wygląda dość symetrycznie, ale w praktyce każda z tych struktur służy do innego typu problemu. To właśnie tu najczęściej pojawiają się dobre i złe decyzje projektowe.
List, gdy liczy się pozycja
`List` przechowuje elementy w konkretnej kolejności i pozwala odwoływać się do nich przez indeks. To świetny wybór, gdy kolejność ma znaczenie, a ten sam element może wystąpić więcej niż raz. Dla mnie to naturalny model dla list zadań, kolejki rezultatów, tablic wynikowych czy danych, które trzeba iterować od początku do końca.
Najpopularniejsza implementacja to `ArrayList`, bo daje szybki dostęp po indeksie i zwykle dobrze sprawdza się w codziennym kodzie. `LinkedList` wygląda atrakcyjnie tylko na papierze; jeśli nie pracujesz intensywnie na obu końcach struktury, często jest po prostu gorszym wyborem niż `ArrayList`.
Set, gdy ważna jest unikalność
`Set` usuwa problem duplikatów u źródła. Jeśli chcesz mieć tylko różne wartości, nie ma sensu budować tego ręcznie na `List` i później filtrować danych. W praktyce to przydatne przy tagach, identyfikatorach, uprawnieniach albo zbiorach rekordów, gdzie powtórki są błędem, a nie cechą danych.
Tu również liczy się implementacja. `HashSet` daje prostą i szybką pracę bez gwarancji kolejności, `LinkedHashSet` zachowuje kolejność wstawiania, a `TreeSet` utrzymuje sortowanie. Jeśli potrzebujesz porządku rosnącego albo porównywania według własnej reguły, wchodzisz już w świat `SortedSet` i `NavigableSet`.
Przeczytaj również: Java switch case - jak pisać, by uniknąć błędów i pisać lepiej?
Queue i Deque, gdy liczy się kolejność przetwarzania
`Queue` służy do przetwarzania elementów w określonym porządku, najczęściej FIFO, czyli pierwszy wchodzi, pierwszy wychodzi. `Deque` idzie krok dalej, bo pozwala operować na obu końcach. To właśnie dlatego bywa używany zarówno jako kolejka, jak i jako stos, bez sięgania po starsze, mniej wygodne API.
Jeśli miałbym wskazać domyślny wybór, zwykle zaczynam od `ArrayDeque`. Jest prosty, szybki i dobrze pasuje do zadań, w których dodajesz oraz zdejmujesz elementy z początku lub końca. `PriorityQueue` wybierasz dopiero wtedy, gdy kolejność przetwarzania ma wynikać z priorytetu, a nie z samego czasu dodania.
Po tej części dobrze widać, że `Collection` nie jest jedną klasą „do wszystkiego”, tylko zestawem kontraktów o różnych właściwościach. Następny krok to osobna gałąź frameworka, czyli `Map`.
Dlaczego Map stoi osobno i kiedy warto po nią sięgnąć
`Map` nie jest zbiorem elementów, tylko strukturą do pracy na parach klucz-wartość. To fundamentalna różnica. Kiedy potrzebujesz pobierać dane po unikalnym identyfikatorze, nazwie, kodzie albo innym kluczu, `Map` zwykle jest właściwym narzędziem od pierwszego szkicu kodu.
Ważne jest też to, że mapa udostępnia widoki danych przez `keySet()`, `values()` i `entrySet()`. Dzięki temu możesz iterować po kluczach, wartościach lub parach, ale nadal nie staje się ona podtypem `Collection`. To osobny kontrakt, z własną logiką i własnymi kompromisami.
| Implementacja | Porządek | Typowa charakterystyka | Kiedy ma sens |
|---|---|---|---|
| `HashMap` | Brak gwarancji | Średnio szybki dostęp po kluczu | Gdy liczy się prosty i szybki lookup |
| `LinkedHashMap` | Kolejność wstawiania lub dostępu | Porządek przy zachowaniu wygodnego dostępu | Gdy chcesz stabilnego przebiegu iteracji |
| `TreeMap` | Posortowany | Uporządkowanie kosztem większego narzutu | Gdy wynik ma być zawsze w kolejności rosnącej |
W praktyce mapa świetnie sprawdza się w słownikach, cache, konfiguracjach, licznikach i wszędzie tam, gdzie jeden klucz ma prowadzić do jednej wartości. Jeżeli próbujesz modelować taki problem listą, zwykle kończy się to dodatkowym kodem, który tylko udaje prostotę. Następny krok to już decyzja, jak wybrać implementację bez zgadywania.
Jak wybrać implementację bez zgadywania
Ja zwykle zaczynam od czterech pytań: czy potrzebuję duplikatów, czy muszę zachować kolejność, czy dane mają być sortowane i czy kluczową operacją jest odczyt po indeksie, czy po kluczu. To znacznie lepszy punkt startu niż pytanie „która klasa jest popularna”.
- Czy element może wystąpić więcej niż raz?
- Czy kolejność wstawiania ma być widoczna przy iteracji?
- Czy potrzebuję sortowania albo szybkiego dostępu po kluczu?
- Czy będę częściej dodawać i usuwać elementy, czy raczej je czytać?
| Potrzeba | Dobry punkt startu | Dlaczego | Na co uważać |
|---|---|---|---|
| Odczyt po indeksie | `ArrayList` | Szybki dostęp do pozycji | Wstawianie w środku kosztuje więcej |
| Unikalne elementy | `HashSet` | Prosto eliminuje duplikaty | Brak gwarancji kolejności |
| Unikalne elementy z kolejnością | `LinkedHashSet` | Zachowuje kolejność wstawiania | Nie zastępuje sortowania |
| Pary klucz-wartość | `HashMap` | Naturalny model do lookupu | Nie zakładaj porządku iteracji |
| Wynik posortowany | `TreeSet` / `TreeMap` | Porządek jest częścią kontraktu | Operacje są cięższe niż w strukturach hash-based |
| Kolejka lub stos | `ArrayDeque` | Praktyczna i szybka do pracy na końcach | To nie jest lista z losowym dostępem |
Takie podejście daje lepszy efekt niż zapamiętywanie nazw klas na skróty. Dobierasz strukturę do problemu, a nie problem do struktury. To właśnie tutaj hierarchia kolekcji zaczyna oszczędzać czas, zamiast go zabierać.
Najczęstsze błędy, które później kosztują czas
Najczęściej widzę kilka powtarzalnych pomyłek, które psują czytelność kodu albo prowadzą do cichych błędów. Dobra wiadomość jest taka, że większość z nich da się wyłapać już na etapie wyboru typu.
- Mylenie `Collections` z `Collection` - pierwsze to klasa z metodami narzędziowymi, drugie to interfejs opisujący grupę elementów.
- Zakładanie, że `HashMap` lub `HashSet` zachowają kolejność - jeśli porządek ma znaczenie, trzeba wybrać inną implementację.
- Wybieranie `LinkedList` „na wszelki wypadek” - w codziennym kodzie często przegrywa z `ArrayList` albo `ArrayDeque`.
- Trzymanie obiektów jako kluczy w `Map`, a potem zmienianie pól używanych w `equals()` i `hashCode()` - to proszenie się o trudne do znalezienia błędy.
- Używanie `List` do danych, które z definicji powinny być unikalne - później trzeba dorabiać filtrowanie i walidację.
- Próba indeksowania `LinkedList` tak jak `ArrayList` - te struktury mają zupełnie inny profil kosztów.
- Zakładanie, że każda implementacja toleruje `null` tak samo - to zależy od konkretnej klasy, nie od samej idei kolekcji.
Gdy unikasz tych błędów, kod szybciej staje się przewidywalny. I właśnie dlatego warto znać nie tylko nazwy klas, ale też logikę ich działania. Ostatni element układanki to współczesne rozszerzenia hierarchii, które porządkują pracę z kolekcjami uporządkowanymi.
Jak nowe interfejsy porządkują współczesną hierarchię
Od JDK 21 hierarchia została rozszerzona o `SequencedCollection`, `SequencedSet` i `SequencedMap`. Ich sens jest prosty: kiedy struktura ma określony porządek przejścia, API powinno dawać spójny dostęp do pierwszego elementu, ostatniego elementu i odwróconej kolejności.
To ważne nie dlatego, że nagle trzeba przepisywać cały kod, ale dlatego, że framework stał się bardziej konsekwentny. Jeśli pracujesz na strukturach takich jak `List`, `Deque`, `LinkedHashSet` czy `LinkedHashMap`, nowe interfejsy lepiej opisują ich zachowanie niż starsze, bardziej rozproszone podejście.
Najpraktyczniejsza zasada jest prosta: najpierw zdecyduj, czy potrzebujesz kolejności, unikalności czy par klucz-wartość, a dopiero potem wybieraj konkretną klasę. W Javie dobry wybór kolekcji zwykle daje większy zysk niż późniejsze „optymalizowanie” już napisanego kodu.