Wstęp do kolekcji

AUTOR: MAREK SOWA

 

W skrócie, kolekcje w Javie służą do przechowywania zbiorów elementów. Działają podobnie jak tablice, ale w przeciwieństwie do nich są dynamiczne. Kolekcje, w przeciwieństwie do tablic, nie mają takiego ograniczenia, jak rozmiar. Posiadają mnóstwo przydatnych metod w zależności od typu i generalnie można z nimi robić więcej niż z tablicami.

Pamiętajmy jednak, że kolekcje są wyżej-poziomowe niż tablice i w większości przypadków są wolniejsze, a już na pewno wymagają większej ilości pamięci komputerowej. Kolekcjami nazywamy klasy, które rozszerzają interfejs ‘Collection’, dziedziczą po nim:

KolekcjeDziedziczenie

Wielu programistów sprzecza się o Mapy, jedni uważają je za kolekcje inni nie. Są argumenty za i przeciw. Mapy omówimy w osobnym wpisie, ponieważ, jak widzimy, nie dziedziczą po Collection<E>, a są bardziej skomplikowane niż standardowe kolekcje. 

Zapewne zauważyliście tajemną literę 'E' zapisaną w nawiasie ostrokątnym '<>'. Taki zapis oznacza typ generyczny, który również zostanie omówiony w innym wpisie. Teraz powinna wystarczyć wiedza, że pod literę 'E' można podstawić dowolny złożony typ danych jak na przykład String, czyli: Collections<String> będzie oznaczać, że kolekcja przechowuje zbiór stringów. Na przykład typ złożony od typu prostego int to Integer, char to Character a double to Double itp.

Dzisiaj omówimy trzy główne typy kolekcji: Listy, Sety i Kolejki. Kolekcje te posiadają inne cechy i różnią się od siebie rodzajami implementacji.

transport-2292028_1920

Listy (java.util.List)

Listy są najpopularniejszymi kolekcjami w Javie. Charakteryzują się tym, że każdy element ma przyporządkowany numer czyli posiada indeks. W liście możemy przechowywać wiele razy ten sam obiekt. Są najbardziej podobne do tablic. Wyobraźcie sobie, ze budujecie pociąg. Wybierając tablicę, musicie powiedzieć inżynierom, ile będzie do niego doczepionych wagoników. W przypadku listy inżynierowie zbudują samą lokomotywę i będą dobudowywać kolejne wagony, gdy pojawią się pasażerowie. Konstrukcja najpopularniejszej listy ArrayList wygląda tak:  

List<String> train = new ArrayList<>();

Jak w przypadku każdego obiektu, tak i tu, musimy nadać mu nazwę. U nas będzie to 'train'. Deklarujemy nasz typ danych, który będzie przechowywała lista, czyli <String>. Od teraz "lokomotywa" istnieje i czeka na "pasażerów". Pobawmy się naszym pociągiem trochę i dodajmy kilka elementów do listy:

String nameOfBolek = "Bolek";
List<String> train= new ArrayList<>();
System.out.println(train.isEmpty()); // true
train.add("Lolek");
train.add(nameOfBolek);
System.out.println(train.isEmpty()); // false
System.out.println(train.get(0)); // Lolek
System.out.println(train.size()); // 2
train.remove(0);
System.out.println(train.size()); // 1

Metoda .add() dodaje kolejny element do listy, 


.remove() usuwa go,


.get() pobiera element z listy,


.size() informuje nas o wielkości zbioru,


a .isEmpty() czy jest pusty.

Zachęcam do wypróbowania wszystkich metod poprzez wpisanie '.' kropki na końcu nazwy obiektu. Większość IDE podpowie jakie metody można użyć.

Kolejnym typem listy jest LinkedList, która przechowuje kolejne elementy, wiążąc je ze sobą tak, że pierwszy element, wie gdzie jest drugi, drugi wie gdzie jest trzeci itd. Ten typ listy jest szybszy w przypadku, gdy dodajemy bardzo dużą ilość pojedynczych elementów.

Zalecam jednak stosowanie ArrayList, która jest najbardziej uniwersalna.

Warto dodać o trzecim typie list, czyli Vectorktóry jest przestarzały. Zgodnie z dokumentacją Javy zaleca się już z niego zrezygnować.  

abstract-21523_1920

 

Set (java.util.Set)

Sety charakteryzują się tym, że elementy się w nich znajdujące nie mają indeksu, a elementy w secie nie mogą się powtarzać. Dostęp do nich uzyskujemy za pośrednictwem iteratora.

Sety świetnie sprawdzają się w sytuacjach, gdy nie ma znaczenia, gdzie znajduje się kolejny element. Jednocześnie jednak zależy nam na tym, by te elementy się tam znajdowały i były unikalne.

Wyobraźmy sobie, że chcemy stworzyć sobie unikalną kolekcję kart, którą gromadziliśmy latami w pudłach, które pełniły funkcję listy. Kart mamy całe mnóstwo. Wiemy, że wiele z nich się dubluje, a drugi egzemplarz nie jest na do niczego potrzebny. Set to taki dobry kolega, któremu możemy przekazać całe pudła powtarzających się kart, a on nie będzie przechowywał powtórzonych. Unikalne karty możemy potem powkładać spokojnie do albumu. Dzięki ci secie! =]

Poniżej konstrukcja najbardziej powszechnego setu HashSet<>:

Set<Integer> numbers = new HashSet<>();
System.out.println(numbers.isEmpty()); // true
numbers.add(2);
numbers.add(1);
numbers.add(2);
numbers.add(4);
numbers.add(2);
System.out.println(numbers.isEmpty()); // false
System.out.println(numbers.size()); // 3

Wyżej przedstawione metody posiadają identyczne działanie jak w przypadku list. Pamiętajmy, że elementy są nieuporządkowane i używając metody .get(), za każdym razem gdy coś dodamy/usuniemy ze zbioru, możemy otrzymywać inne wyniki.

Innym typem setu jest LinkedHashSet<>, który różni się od HashSet<> tym, że w nim elementy są uporządkowane podobnie jak w ArrayList<>. Iterator w LinkedHashSet<> będzie przechodził przez kolejne elementy zawsze w tej samej kolejności.

Ostatnim typem setów jest TreeSet<>. Jest on specyficzny, ponieważ sortuje przechowywane elementy. Sortowanie odbywa się według porządku naturalnego w przypadku, w którym elementy w zbiorze implementują interfejs Comperable. Niestety ten typ kolekcji potrzebuje dużej mocy obliczeniowej na każdą operację. Każde dodanie elementu uruchamia proces sortowania.

kolejka

Kolejki (java.util.Queue)

Ostatnim omawianym typem kolekcji są kolejki. W kolejkach wyróżniamy głowę (head) oraz ogon (tail). W kolejkach możemy pracować tylko nad początkiem kolejki albo nad końcem. Idea kolejek stanowi przetwarzanie jej elementów w określonej kolejności.

Wyróżnia się dwa typy kolejek:


  • LIFO (last-in, first-out) -  często porównuje się do stosu, do którego wrzucamy wiele elementów ale jak chcemy z tego stosu wyciągnąć jakiś element to sięgamy po ten pierwszy z góry, czyli ostatni wrzucony.


  • FIFO (first-in, first-out) - Fifo możemy wyobrazić sobie jako kolejkę do sklepu w czasach PRL. Kto pierwszy przyszedł i się ustawił ten pierwszy będzie obsłużony.


 

Najpopularniejszą implementacją kolejki jest ArrayDeque<>. Przechowuje ona elementy zgodnie z ich kolejnością dodawania i umożliwia działania od strony głowy. To kolejka typu FIFO. Konstrukcja wygląda tak:

Queue<Integer> numbers = new ArrayDeque<>();
System.out.println(numbers.isEmpty()); // true
numbers.add(3);
numbers.add(1);
numbers.add(2);
numbers.add(4);
numbers.add(5);
System.out.println(numbers.isEmpty()); // false
System.out.println(numbers.size()); // 5
System.out.println(numbers.remove()); // 3 - pierwszy element kolejki
System.out.println(numbers.size()); // 4
System.out.println(numbers.peek()); // 1 - pierwszy po usunieciu 3
System.out.println(numbers.size()); // 4

Jak widzimy na przykładzie powyżej, do kolejki również można dodawać elementy metodą .add(). Usuwanie elementu następuje po użyciu metody bezargumentowej .remove(), gdzie nie wskazujemy indeksu do usunięcia. W tej kolejce pobierany i usuwany zostaje zawsze pierwszy element. Do samego podejrzenia pierwszego elementu w kolejce używamy metody .peek()

Drugą implementacją kolejki jest PriorityQueue<>. Wyróżnia się tym, że sortuje dodawane elementy w zależności od priorytetu. Taka kolejka świetnie spisałaby się w przypadku, gdy mamy zbiór produktów spożywczych z określoną datą przydatności do spożycia. W pierwszej kolejności taki zbiór podawałby nam produkty z najkrótszą datą, bo mają najwyższy priorytet. Dodatkową zaletą tej kolejki jest nie przechowywanie wartości null.

Trzecią implementacją kolejki jest Stack<>. Jest to kolejka typu LIFO, stos. Ciekawostką jest, że Stack<> jest nie zgodny z Queue, a z List. Stack<> jest obecnie przestarzały podobnie jak Vector<>, zgodnie z dokumentacją Javy zaleca się korzystanie z ArrayDequeue<>