Kurs java dla początkujących - #4 kolekcje

Kurs Java dla początkujących – #4 Kolekcje

Kolejna część kursu będzie traktować o kolekcjach, czyli o tym, z czym każdy programista pracuje na co dzień. Kolekcje to taki element języka Java, który każdy programista powinien mieć opanowany do perfekcji. Dzisiaj omówię tylko podstawowe kolekcje z biblioteki standardowej, ale musisz wiedzieć, że jest ich znacznie więcej i ciągle powstają nowe.

Czym są kolekcje?

W poprzedniej części kursu pisałem o podstawowych kontenerach danych jakimi są tablice. Kolekcje są w uproszczeniu takimi „tablicami na sterydach”. Czyli są kontenerami na dane o bardziej zaawansowanych możliwościach. A sposób składowania danych w kolekcjach zależy od implementacji.

Głównymi zaletami kolekcji są:

  • elastyczność – rozmiar kolekcji zmienia się automatycznie w zależności od potrzeb;
  • zaawansowanie – łatwość operowania na danych, które przechowują.

W języku Java za implementację kolekcji odpowiada tzw. Collections Framework – jest to zbiór wszystkich dostępnych kolekcji w Javie, których implementacja została stworzona wiele lat temu i jest udoskonalana do dzisiaj.  Collections Framework definiuje także podstawowe interfejsy dla kolekcji np. List, Map, Set (o interfejsach będzie później). W bibliotece standardowej mamy także dostępną specjalną klasę Collections, która zawiera implementację standardowych operacji wykonywanych na kolekcjach.

Interfejs – interfejs w Javie jest szablonem dla klasy. Zawiera on definicje metod publicznych, ale nie zawiera ich implementacji (są jednak od tego wyjątki, metody statyczne i metody domyślne). Jeśli klasa implementuje dany interfejs powinna także implementować wszystkie metody publiczne tego interfejsu.

public interface Animal {
  void walk();
}

Implementacja interfejsu

class Dog implements Animal {
  public void walk() {
    System.out.println("Pies chodzi na 4 łapach");
  }
}

Implementacje poszczególnych typów kolekcji

Typy generyczne

Zanim przejdziemy do implementacji poszczególnych kolekcji konieczne jest zapoznanie się z pojęciem typy generyczne, które zostało wprowadzone do języka Java w wersji 1.5. Typy generyczne są wykorzystywane w kolekcjach. Inaczej mówiąc są to typy szablonowe, które pomagają nam wykrywać problemy związane z typami na poziomie kompilacji (czyli najwcześniej jak się da).

Do oznaczania typu generycznego używamy nawiasów trójkątnych z literką oznaczającą nazwę naszego typu np. <T>. Zwykle są to litery: T, E, K, V, R (ale może to być dowolny ciąg znaków). Pozwala to na obsłużenie wielu typów przez jedną klasę (klasa staje się szablonem).

W deklaracji klasy definiujemy jakiś abstrakcyjny typ T, który będzie obsługiwała nasza klasa.

class List<T> {
// implemetacja metod pominięta
}

Tworząc instancję klasy z typem generycznym podajemy już konkretny typ jakiego chcemy używać np. String

//bez typów generycznych lista może zawierać obiekty dowolnego typu
List lista = new ArrayList();

// z typami generycznymi lista może zawierać tylko obiekty typu String
List<String> listaStringow = new ArrayList<>();

// inna lista
List<Integer> listaIntegerów = new ArrayList<>();

Użycie typów generycznych daje nam gwarancję, że dana klasa (szablonowa) w danym miejscu obsługuje tylko ten jeden konkretny typ.

Lista

Listy są najczęściej używanymi kolekcjami w Javie. W bibliotece standardowej jest dostępnych kilka podstawowych implementacji interfejsu List. Dwie najczęściej używane to ArrayList i LinkedList (udostępniają te same metody ponieważ implementują ten sam interfejs). Różne implementacje kolekcji zwykle powstają dlatego, że w różnych zastosowaniach jedne sprawdzają się lepiej a drugie gorzej. Jeśli nie wiesz jakiej implementacji listy użyć, użyj ArrayList jest ona na tyle uniwersalna, że sprawdza się w 99% przypadków.

Ale czym jest lista? Lista jest zbiorem nieunikalnych elementów, które są domyślnie posortowane w kolejności dodawania.

Listę inicjalizujemy podając typ wartości:

List<String> cities = new ArrayList<>(); // wartości elementów listy będą typu String

Interfejs List udostępnia nam kilka przydatnych metod pozwalających operować na listach:

.add(E element) – pozwala dodawać pojedynczy element na koniec listy.
cities.add("Warszawa");
cities.add("Poznań");
cities.add("Kraków");

.get(int index) – pozwala pobrać element, który znajduje się pod konkretnym indeksem listy

cities.get(0); // pobiera pierwszy element z listy w tym wypadku: Warszawa

.contains(Object o) – pozwala sprawdzić czy danych element znajduje się na liście. Przydaje się często w instrukcjach warunkowych (o tym w następnej części kursu).

System.out.println(cities.contains("Warszawa")); // wyświetli: true
System.out.println(cities.contains("Gdańsk")); // wyświetli: false

.isEmpty() – pozwala sprawdzić czy lista jest pusta

System.out.println(cities.isEmpty()); // wyświetli: false

.size() – pozwala sprawdzić rozmiar listy

System.out.println(cities.size()); // wyświetli: 3

.remove(int index) – usuwa element pod wybranym indeksem

System.out.println(cities.remove(0)); // wyświetli: 0

.sort(Comparator<? super E> c) – sortuje listę przy użyciu comparatora

cities.sort(Comparator.naturalOrder()); // posortuje listę alfabetycznie
To są najczęściej używane metody listy. Jest ich oczywiście znacznie więcej. Natomiast w przyszłości zapewne poznasz większość z nich.

Przeglądanie zawartości listy

Aby wyświetlić wszystkie elementy listy, możemy to zrobić w analogiczny sposób jak w przypadku tablic. Czyli przy użyciu pętli for.
for(int i=0; i < cities.size(); i++) { // korzystamy z metody .size()
  System.out.println(cities.get(i)); // korzystamy z metody .get(int index)
}

Zbiór

Zbiór (Set) jest bardzo podobny do listy. Może przechowywać wartości, z tą różnicą, że zbiór przechowuje tylko unikalne wartości. Drugą różnicą jest to, że nie zachowuje kolejności wstawiania elementów (tak jak robi to lista). Nie zachowuje, ale może to robić, w zależności od wybranej implementacji. Najczęściej używaną implementacją zbioru jest HashSet, klasa ta implementuje interfejs Set (implementacja ta nie zachowuje żadnej kolejności elementów). Implementacją, która zachowuje kolejność wstawiania elementów jest LinkedHashSet (także jest implementacja interfejsu Set).

Zbiór inicjalizujemy podobnie jak listę podając typ wartości:

Set<String> cities = new HashSet<>();

Najbardziej przydatne metody w zbiorach to:

.add(E element) – pozwala dodać element do zbioru, jeżeli element już istnieje nie jest dodawany ponownie

cities.add("Warszawa");
cities.add("Poznań");
cities.add("Warszawa");

.contains(Object o) – pozwala sprawdzić czy podany obiekt znajduje się w zbiorze

System.out.println(cities.contains("Warszawa")); // wyświetli: true
System.out.println(cities.contains("Gdańsk")); // wyświetli: false

.isEmpty() – pozwala sprawdzić, czy zbiór jest pusty

System.out.println(cities.isEmpty()); // wyświetli: false

.size() – pozwala sprawdzić rozmiar zbioru

System.out.println(cities.size()); // wyświetli: 2

.remove(Object o) – pozwala usunąć podany obiekty ze zbioru

System.out.println(cities.remove("Poznań")); // wyświetli: true

Przeglądanie zawartości zbioru

Jak pewnie zauważyłeś zbiór nie posiada metody .get(int index), która pozwoliłaby w wygodny sposób pobrać dowolny element, tak jak jest to w liście. Jak więc możemy pobrać elementy ze zbioru? Jest na to kilka rozwiązań. Możemy użyć „specjalnego mechanizmu” iteratora lub skorzystać z tak zwanej pętli foreach.

foreach – pętla ta swoją konstrukcją trochę się różni od zwykłej pętli for. Zaczyna się ona standardowo słowem kluczowym for

for(E element : collection) {
  // tutaj kod
}

dalej mamy zmienną E element, która w kolejnych obrotach pętli będzie przyjmowała wartości poszczególnych elementów zbioru : (dwukropek), który jest separatorem i na końcu, kolekcję collection po której iterujemy, może to być lista, zbiór lub tablica.

Przykład użycia pętli foreach z użyciem naszej listy:

for (String city : cities) {
  System.out.println(city);
}

Mapa

Mapa jest strukturą klucz-wartość, co sprawia, że jest doskonała do przechowywania wszelkiego rodzaju danych słownikowych lub indeksowanych. Najpopularniejszą implementacja interfejsu Map jest HashMap.

Mapę inicjalizujemy podając typy dla klucza i wartości:

Map<Integer, String> map = new HashMap<>(); // klucz jest typu Integer a wartość typu String

Podstawowe najbardziej przydatne metody w mapach to:

.put(K key, V value) – pod kluczem key pozwala wstawić wartość value

map.put(1, "Pierwsza wartość");
map.put(2, "Druga wartość");
map.put(3, "Trzecia wartość");

.get(Object key) – zwraca wartość zapisaną pod kluczem key lub null jeśli nie odnaleziono podanego klucza

map.get(1); // zwróci: Pierwsza wartość

.remove(Object key) – usuwa wartość przypisaną do klucza key. Zwraca wartość która byłą przypisana do tego klucza lub null jeśli nie znaleziono wartości dla tego klucza.

map.remove(1) // zwóci: Pierwsza wartość
map.remove(4) // zwóci: null

.isEmpty() – sprawdza, czy mapa jest pusta

System.out.println(map.isEmpty()); // wyświetli: false

.size() – pozwala sprawdzić rozmiar mapy

System.out.println(map.size()); // wyświetli: 3

.keySet() – zwraca zbiór kluczy

.values() – zwraca kolekcję wartości

.entrySet() – zwraca zbiór obiektów typu Map.Entry (Set<Map.Entry<K,V>>), które reprezentują kolejne pary klucz-wartość w mapie

Przeglądanie zawartości map

Do przeglądania zawartości map kluczowe są trzy powyższe metody: .keySet(), .values() oraz .entrySet(). Korzystając z nich oraz pętli foreach możemy łatwo wyświetlić całą zawartość mapy.

Najczęściej przeglądamy wartości zawarte w mapie. Korzystamy w tedy z metody .values():

for(String el : map.values()) {
  System.out.println(el);
}

Gdy potrzebujemy jednocześnie klucza i wartości korzystamy z metody .entrySet():

for(Map.Entry<Integer, String> entry : map.entrySet()) {
  System.out.println(entry.getKey() + " : " + entry.getValue());
}

Gdy potrzeba nam samych kluczy używamy metody .keySet():

for(Integer key : map.keySet()) {
  System.out.println(key);
}

Wady kolekcji

Na początku pisałem o zaletach kolekcji. Teraz przyszedł czas, żeby wspomnieć o wadach. Jedną z głównych wad kolekcji jest to, że nie obsługują one typów prostych takich jak int, float, double, long. Spowodowane jest to tym, że kolekcje są zbudowane na bazie typów generycznych, które nie działają z typami prostymi. Jest łatwe rozwiązanie tego problemu, czyli wrapper’y dla typów prostych. Odpowiednio: Integer, Float, Double, Long, niestety konsekwencją tego rozwiązania jest większe zużycie pamięci, ale w większości przypadków nie stanowi to dużego problemu dzięki automatycznemu odśmiecaniu pamięci w Javie.

Kolejną wadą jest czasem sama implementacja kolekcji, która może nie być z natury optymalna. Np. LinkedList – jej implementacja nie jest optymalna, ponieważ dla każdego jej elementu jest potrzebny dodatkowy obiekty typu Node<E>, co sprawia, że ta implementacja jest o wiele bardziej pamięciożerna niż np. ArrayList, której implementacja jest oparta na zwykłej tablicy i nie potrzebuje dodatkowych obiektów (wrapper’ów).

Sortowane Kolekcji

Sortowanie list jest bardzo proste, wystarczy użyć metody .sort(Comparator<? super E> c) i jako parametr przekazać odpowiedni comparator lub po prostu wpisać null. Dzięki temu zostanie zastosowane sortowanie naturalne (w wypadku obiektów klasy String – alfabetyczne).

Comparator to specjalny obiekt służący do porównywania dwóch obiektów tego samego typu. Zawiera on tylko jedną metodę: compare(T object1, T object2), która zwraca 0(zero) gdy dwa obiekty są równe. Wartość większą od zera gdy pierwszy jest większy niż drugi, oraz wartość mniejszą od zera gdy pierwszy jest mniejszy od drugiego.

Comparatory definiujemy używając interfejsu Comparator, który ma tylko jedną metodę compare. W bibliotece standardowej mamy zdefiniowanych kilka podstawowych comparatorów np. Comparator.naturalOrder(), Comparator.nullsFirst(), Comparator.nullsLast().

Oczywiście, mamy też możliwość napisania własnego comparatora w postaci lambdy (o lambdach i comparatorach będzie więcej w kolejnych częściach kursu).

list.sort((o1, o2) -> o1.compareTo(o2));

Z innymi kolekcjami jest trochę gorzej. HashMap i HashSet nie mogą być sortowane, ponieważ implementacja i same interfejsy tego nie wspierają. Musimy więc użyć odpowiednich implementacji, które sortują elementy w momencie dodawania ich do kolekcji. Takie kolekcje to odpowiednio TreeMap, która implementuje interfejs SortedMap i TreeSet, który implementuje interfejs SortedSet.

Klasa narzędziowa Collections

Biblioteka standardowa wyposażona jest w specjalną klasę narzędziową Collections, która zawiera listę specjalnych metod pomagających wykonywać różne dodatkowe operacje na kolekcjach. Warto zapoznać się z tą klasą bardzo dokładnie, dzięki temu unikniesz w przyszłości samodzielnego implementowania, różnych operacji np. odwracania listy.

Collections.reverse(List<?> list) – pozwala odwrócić kolejność elementów w liście

Collections.copy(List<? super T> dest, List<? extends T> src) – pozwala na skopiowanie kolekcji do innej kolekcji.

Collections.sort(List<T> list) – metoda, która opakowuje metodę .sort(Comparator<? super E> c) z interfejsu List z null' owym parametrem – jest to trochę ładniejszy zapis tego wywołania

Collections.replaceAll(List<T> list, T oldVal, T newVal) – pozwala zamienić dopasowane elementy na nowy element

Collections.min(Collection<? extends T> coll) – zwraca minimalny element z kolekcji

Collections.max(Collection<? extends T> coll) – zwraca maksymalny element z kolekcji

Klasa ta zawiera jeszcze wiele innych metod. Pełna lista dostępna w dokumentacji.

Podsumowanie

W samej bibliotece standardowej jest jeszcze wiele różnych ciekawych implementacji różnych kolekcji. Ja przestawiłem tutaj tylko najczęściej wykorzystywane. Te, które każdy programista Javy powinien doskonale znać. Musisz wiedzieć, że jest jeszcze wiele różnych implementacji kolekcji, które są rozwijane przez liczne organizacje lub przez społeczność open source.

Duża ilość niestandardowych kolekcji powstaje dlatego, że w wielu przypadkach standardowe kolekcje są albo za wolne albo brakuje im pewnych specyficznych funkcjonalność, które pomagają rozwiązywać specyficzne problemy. To nie znaczy jednak, że musisz je wszystkie znać. W 99% przypadku wystarczą ci kolekcje z biblioteki standardowej, ale dla tego 1% przypadków warto wiedzieć, że są też inne bardziej specyficzne kolekcje.

 

Co powinien wiedzieć każdy początkujący programista?

 

Kurs Java dla początkujących

Spis Treści:

  1. Wprowadzenie
  2. Klasy i Obiekty
  3. Tablice
  4. Kolekcje
  5. Instrukcje warunkowe i pętle
  6. Operacje wejścia i wyjścia
  7. Dziedziczenie, Polimorfizm, Interfejsy
  8. Stream’y i lambdy

 

Żródła:

https://docs.oracle.com/javase/8/docs/technotes/guides/collections/overview.html
https://docs.oracle.com/javase/tutorial/java/generics/types.html
https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html
https://docs.oracle.com/javase/8/docs/api/java/util/LinkedList.html
https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html
https://docs.oracle.com/javase/8/docs/api/java/util/HashSet.html
https://docs.oracle.com/javase/8/docs/api/java/util/Collections.html

 

Mateusz Dąbrowski

Cześć jestem Mateusz, zajmuję się programowaniem już ponad 12 lat z czego ponad 8 programuję w Javie. Zapraszam Cię do lektury mojego bloga. Możesz przeczytać więcej o mnie >>TUTAJ<<

2 thoughts to “Kurs Java dla początkujących – #4 Kolekcje”

  1. Gratuluję, kolejny dobry artykuł 😉
    Napisałeś, że „Comparator (…) zwraca 0(zero) gdy dwa obiekty są równe. 1(jeden) gdy pierwszy jest większy niż drugi, oraz -1 gdy pierwszy jest mniejszy od drugiego.” A nie jest przypadkiem tak, że zwraca wartość dodatnią lub ujemną, ale niekonicznie +1 lub -1 ?

    1. Dzięki Marcin za komentarz. Masz rację, może to być wartość większa lub mniejsza od zera np. String.compareTo() zwraca tak. Ale w wielu miejscach są stosowane -1, 0, 1 np. BigDecimal.compareTo(). Ja zwykle stosuje -1 i 1, więc stąd ta pomyłka 😉

Komentarze są zamknięte.