Equals i hashCode w języku Java
Porównywanie obiektów, metody equals i hashCode w języku Java
17 kwietnia 2016
operacje na plikach w języku Java
Operacje na plikach w języku Java
17 sierpnia 2016
kolekcje w języku Java

W tym artykule przeczytasz o kolekcjach w języku Java. Dowiesz się czym są kolekcje, kiedy można ich używać. Poznasz podstawowe kolekcje takie jak mapa, zbiór czy lista. Jak zwykle na koniec czekały będą na Ciebie zadania, przy których przećwiczysz materiał opisany w tym artykule. Zwrócę też uwagę na parę skrótów klawiaturowych, które mogą Ci się przydać podczas pracy z kodem. Zapraszam do lektury!

To jest jeden z artykułów w ramach darmowego kursu programowania w Javie. Proszę zapoznaj się z pozostałymi częściami, mogą one być pomocne w zrozumieniu materiału z tego artykułu.

Czym są kolekcje

Java, podobnie jak wiele innych języków, w tak zwanej bibliotece standardowej1 zawiera zestaw kolekcji. Kolekcja to nic innego jak sposób grupowania obiektów. Kolekcją możemy także nazwać tablicę obiektów, którą już znasz. Jednak tego typu kolekcja na pewne ograniczenia, głównym z nich jest to, że rozmiar, który ustalimy na początku nie może być zmieniony.

Kolekcje możemy opisać jako „tablice na sterydach”. Pozwalają one na dużo więcej niż przechowywanie obiektów w kolejności określonej przez tablicę.

Kolekcje to implementacje tak zwanych struktur danych. O przykładowych implementacjach struktur danych  na pewno przeczytasz w jednym z kolejnych artykułów. Na dzisiaj musisz zapamiętać, ze rodzaj kolekcji/struktury danych pozwala napisać program, który jest bardziej bądź mniej wydajny (działa szybciej lub wolniej). To, jaką kolekcję w danym momencie użyjemy ma znaczenie. W kolejnych akapitach postaram się przytoczyć podstawowe kolekcje wraz z przykładami ich użycia.

Hierarchia dziedziczenia

Kolekcje w standardowej bibliotece Javy implementują różne interfejsy, poniższy diagram pokazuje  hierarchię dziedziczenia dla podstawowych typów kolekcji dostępnych w Javie. Każdy z tych interfejsów ma kilka implementacji, których używa się w różnych sytuacjach.

dziedziczenie kolekcji w języku Java

Lista

Lista (ang. list) podobnie jak tablica, grupuje elementy. Jej główną przewagą nad tablicą jest to, że programista nie musi się przejmować rozmiarem listy2, jest ona automatycznie powiększana wraz z dodawaniem nowych elementów. Listy w języku Java reprezentowane są przez interfejs java.util.List. Listy z definicji są kolekcjami dla których kolejność elementów jest istotna, mogą przechowywać ten sam obiekt po kilka razy. Podstawowymi przykładami implementacji interfejsu java.util.List są klasy java.util.LinkedList oraz java.util.ArrayList.

Bez wdawania się w zbędne szczegóły, proszę zapamiętaj, że LinkedList lepiej jest używać jeśli często usuwasz elementy z listy a ArrayList lepiej jest używać jeśli często chcesz mieć dostęp do losowych elementów w liście. Obiecuje, że dokładne wytłumaczenie  dlaczego tak się dzieje znajdziesz w jednym z kolejnych artykułów.

Przydatne metody w java.util.List

Chciałbym pokazać Ci parę metod, które mogą Ci się przydać przy pracy z listami. Jeśli jesteś zainteresowany pełną listą zachęcam do przeczytania dokumentacji dla interfejsu List, tam znajdziesz wszystkie niezbędne szczegóły.

Załóżmy, że nasza zmienna jest typu List<String>. Wówczas będziesz mógł używać m.in. następujących metod:

  • add – dodaje element do listy,
  • addAll – jako parametr przyjmuje inną kolekcję i dodaje wszystkie elementy z tej kolekcji do listy,
  • contains – jako parametr przyjmuje element listy i zwraca flagę informującą czy dany element już istnieje (tutaj przyda Ci się artykuł o porównywaniu obiektów w języku Java),
  • isEmpty – bezargumentowa metoda zwracająca flagę informującą czy lista jest pusta,
  • size – bezargumentowa metoda zwracająca liczbę elementów w liście,
  • indexOf – metoda jako parametr przyjmuje element listy i zwraca indeks pierwszego wystąpienia,
  • lastIndexOf – metoda jako parametr przyjmuje element listy i zwraca indeks ostatniego wystąpienia.

Opisane wyżej metody użyte zostały w kodzie poniżej.

Tutaj drobna dygresja, w kodzie wyżej widzisz zapis List<String> listWithNames = new LinkedList<>(). Dlaczego nie napisać LinkedList<String> listWithNames = new LinkedList<>()? Pierwsza wersja pokazuje dobrą praktykę, która polega na definiowaniu zmiennych typu interfejsu a nie klasy implementującej ten interfejs. Dzięki temu w przyszłości z łatwością moglibyśmy przypisać do listWithNames zmienną typu ArrayList<String> bez konieczności zmiany pozostałej części programu.

Zbiór

Zbiór (ang. set) to kolekcja, która służy do przechowywania unikalnych elementów. Zbiory w języku Java implementują interfejs java.util.Set. W przypadku zbioru nie jest istotna kolejność dodawanych elementów. Innymi słowy jeśli do zbioru dodamy na początku element X a później Y to przechodząc po kolei po elementach zbioru możemy dostać je w odwrotnej kolejności. Istnieją także implementacje zbioru, w których kolejność elementów jest zachowana, jednak jest to raczej „szczegół implementacyjny” niż szczególna właściwość zbiorów.

Kolejną cechą zbioru jest to, że przechowuje on unikalne elementy. W odróżnieniu od listy, w zbiorze można przechowywać wyłącznie jedną instancję obiektu.

Skąd możemy wiedzieć, że dana instancja jest już w zbiorze? Otóż służą do tego opisane już metody hashCode oraz equals.

Jeszcze raz przypomnę o kontrakcie między tymi metodami. Poprawne działanie kolekcji wymaga poprawnie zaimplementowanych metod hashCode/equals. Jeśli ten warunek nie jest spełniony niektóre kolekcje mogą działać w dziwny, niespodziewany sposób.

Najważniejszą implementacją interfejsu Set jest klasa java.util.HashSet.

Przydatne metody w java.util.Set

Podobnie jak w przypadku list zachęcam do zapoznania się z pełną listą metod dostępnych w interfejsie Set. Poniżej lista kilku przydatnych metod:

  • add – dodaje element do zbioru,
  • addAll – jako parametr przyjmuje inną kolekcję i dodaje wszystkie elementy z tej kolekcji do zbioru (pomijając duplikaty),
  • contains – jako parametr przyjmuje element zbioru i zwraca flagę informującą czy dany element już istnieje,
  • isEmpty – bezargumentowa metoda zwracająca flagę informującą czy zbiór jest pusty,
  • size – metoda zwraca ilość elementów w zbiorze.

Przykład użycia metod znajduje się we fragmencie kodu poniżej.

Mapa

Mapa (ang. map) jest kolekcją, która pozwala przechować odwzorowanie zbioru kluczy na listę wartości. Innymi słowy w mapie możemy trzymać klucze, którym odpowiadają wartości. Klucze muszą być unikalne (dlatego pisałem o zbiorze kluczy), wartości natomiast mogą się powtarzać. Czyli pod kluczem A i pod kluczem B może być ta sama wartość X. Ale sytuacja odwrotna gdzie klucz X występuje dwa razy i jeden z nich wskazuje na element A a inny na element B nie jest możliwa3.

Czytając inne źródła możesz natknąć się na inne nazwy. Słownik, tablica asocjacyjna, mapa – to pojęcia opisujące dokładnie tę samą strukturę danych.

Kluczami w mapie powinny być obiekty, których nie można zmodyfikować (ang. immutable). Np dobrymi kandydatami na klucze są instancje takich klas jak String czy Integer – są to obiekty, których po zainicjalizowaniu nie możemy zmodyfikować. Ponadto klasy kluczy muszą poprawnie implementować metody hashCode/equals. Jeśli jakaś para (klucz, wartość1) istnieje jest w mapie a ty spróbujesz dodać kolejną (klucz, wartość2) (ten sam klucz). Wówczas ta ostatnia para będzie przechowywana przez mapę, nadpisze ona poprzedni element.

Podobnie jak Set i List, Map jest interfejsem generycznym, jednak w tym przypadku wymaga on dwóch klas – pierwsza z nich definiuje typ kluczy, druga typ wartości przechowywanych w mapie.

Standardową implementacją mapy w języku Java jest klasa java.util.HashMap.

Przydatne metody w java.util.Map

  • put – dodaje parę klucz/wartość do mapy,
  • putAll – jako parametr przyjmuje inną mapę i dodaje wszystkie elementy z do mapy,
  • containsKey – jako parametr przyjmuje klucz i zwraca flagę informującą czy dany klucz już istnieje,
  • containsValue – jako parametr przyjmuje wartość i zwraca flagę informującą czy dana wartość już istnieje,
  • isEmpty – bezargumentowa metoda zwracająca flagę informującą czy mapa jest pusta,
  • size – bezargumentowa metoda zwracająca liczbę elementów w mapie,
  • remove – metoda jako parametr przyjmuje klucz i usuwa parę klucz/wartość z mapy,
  • get – metoda jako parametr przyjmuje klucz i zwraca odpowiadającą mu wartość.

Przykłady użycia metod znajdziesz we fragmencie kodu poniżej.

Pełna lista metod dostępna w interfejsie Map znajduje się w dokumentacji.

Skróty klawiaturowe

W IDE, które proponowałem na początku (InteliJ) jest skrót klawiaturowy, który bardzo może Ci się przydać w odkrywaniu nowych metod. Po wpisaniu zmiennej i kropki po niej naciśnij <Ctrl+Spacja&gt; pojawi się menu kontekstowe z dostępnymi atrybutami/metodami tego obiektu.

skróty klawiaturowe intelij idea

Kolejny przydatny skrót klawiaturowy to <Ctrl + H>. Najedź kursorem na interfejs List, po naciśnięciu tego skrótu pojawi się panel zawierający hierarchię dziedziczenia dla elementu pod kursorem. Także ten panel jest widoczny na zrzucie ekranu powyżej.

Dzięki szybkiemu wglądowi w hierarchii dziedziczenia możesz w łatwy sposób odnaleźć inne implementacje danego interfejsu.

Ograniczenia kolekcji

Jak już napisałem wyżej „kolekcje to tablice na sterydach”. Z tymi sterydami przychodzą także pewne ograniczenia. Głównym ograniczeniem jest to że wraz z kolekcjami opisanymi powyżej nie możesz używać typów prymitywnych (Integer tak, int nie). Możesz to łatwo obejść poprzez używanie odpowiadających im obiektów, jednak obiekty takie zajmują więcej miejsca w pamięci niż typy prymitywne.

Istnieją implementacje kolekcji, które pozwalają na używanie typów prymitywnych jednak na tym etapie nauki Javy nie zaprzątałbym sobie nimi głowy. Standardowe kolekcje są w zupełności wystarczające.

Iterowanie po kolekcjach

Z artykułu opisującego pętle dowiedziałeś się o różnych rodzajach pętli i to właśnie na nich tutaj się skupimy4.

Iterowanie po listach

Najprostszym sposobem jest iterowanie przy użyciu pętli foreach. Zgodnie z definicją listy elementy będą zwracane w kolejności dodawania ich do listy.

Jeśli potrzebujemy dostępu do indeksu elementu możemy użyć także standardowej pętli for. Jeśli takie podejście jest wymagane lepiej jest używać implementacji ArrayList niż LinkedList.

Iterowanie po zbiorach

Podobnie jak w przypadku list z pomocą przychodzi pętla foreach, jednak tutaj nie mamy już gwarancji zwrócenia obiektów zbioru w tej samej kolejności, w której były one do niego dodawane (zachęcam do sprawdzenia tego samodzielnie).

Iterowanie po mapach

Z racji tego, że w mapach mamy zbiór kluczy mapowanych na wartości możemy iterować po samych kluczach, samych wartościach bądź parach klucz, wartość. Powinniśmy używać odpowiedniego sposobu w zależności od naszych potrzeb.

Tutaj drobna dygresja, nowością może być dla Ciebie zapis Map.Entry<String, String>. Jest to notacja wskazująca na tak zwaną klasę wewnętrzną (bądź inerfejs). Interfejs Entry został zdefiniowana wewnątrz Map dlatego odwołujemy się do niego poprzez Map.Entry. Jest to interfejs generyczny, który odpowiada parze klucz/wartość, dlatego typowany jest tymi samymi typami co mapa z przykładu.

Porównanie typów kolekcji

Na koniec dla ułatwienia przygotowałem dla Ciebie tabelkę, która grupuje właściwości poszczególnych kolekcji w jednym miejscu wraz z przykładem użycia.

 ListaZbiórMapa
Zachowuje kolejność elementówTakNieNie
Pozwala na przechowywanie kliku takich samych elementów/kluczyTakNieNie
Przykład użycia (podróżowałeś przez Europę pociągiem)Miasta, które odwiedziłeś (cała trasa z drogą powrotną, niektóre miasta oddzwiedziłeś także przy powrocie)Zbiór miast, które odwiedziłeś (bez duplikatów).Nazwy państw, które odwiedziłeś wraz z odpowiadającymi im stolicami.

Zadania

  1. Napisz program, który będzie pobierał od użytkownika imiona. Program powinien pozwolić użytkownikowi na wprowadzenie dowolnej liczby imion (wprowadzenie „-” jako imienia przerwie wprowadzanie). Na zakończenie wypisz liczbę unikalnych imion.
  2. Napisz program, który będzie pobierał od użytkownika imiona par dopóki nie wprowadzi imienia „-”, następnie poproś użytkownika o podanie jednego z wcześniej wprowadzonych imion i wyświetl imię odpowiadającego mu partnera.

Jeśli będziesz miał problemy z rozwiązaniem któregokolwiek z zadań na githubie umieściłem przykładowe rozwiązania. Zachęcam do ich sprawdzenia dopiero po przygotowaniu swojej wersji 🙂

Dodatkowe materiały do nauki

Materiałów na temat kolekcji w internecie jest całkiem sporo, poniżej przygotowałem dla Ciebie zestaw linków do innych blogów/kursów gdzie autorzy także opisują kolekcje. Jeśli będzie brakowało Ci materiałów, bądź będziesz chciał poznać temat z innej strony zachęcam do zapoznania się z nimi. Na początek kod źródłowy przykładów i rozwiązań zadań oraz  dokumentacja biblioteki standardowej.

No i zestaw pozostałych materiałów 🙂

Podsumowanie

Cieszę się, że dotrwałeś do końca. Musisz wiedzieć, że bez kolekcji nie ma programowania, ten artykuł jest naprawdę ważny :). Na koniec mam do Ciebie prośbę, proszę pomóż mi dotrzeć do kolejnych samouków, podziel się z nimi adresem tego bloga, polub stronkę na fb, z góry wielkie dzięki!

Do następnego razu!

Newsletter

  Jeśli chcesz otrzymywać informacje o nowych artykułach na blogu prosto na Twój email, zapisz się 🙂

  1. Biblioteka standardowa to zestaw klas, które może używać programista, dostarczonych wraz z językiem programowania.
  2. Oczywiście w granicach rozsądku, w skrajnych przypadkach utworzenie listy ze zbyt dużą liczbą elementów może prowadzić do wystąpienia błędu OutOfMemoryError.
  3. Tu znów dygresja, oczywiście istnieją implementacje, które pozwalają na takie zachowanie, jednak nie jest to „domyślne” zachowanie.
  4. Na początku pominiemy strumienie, którymi zajmiemy się w osobnym artykule.

9 Komentarze

  1. Łukasx napisał(a):

    Czesc. A może by warto było dopisać krótkie podsumowanie w punktach kiedy i do czego co używać.. tam widziałem że jest wyżej ale w podsumowaniu w punktach usystematyzuje

    • Marcin Pietraszek napisał(a):

      Cześć Łukasx! Dzięki za sugestię, dodałem tabelkę, która grupuje to w jednym miejscu. Proszę daj znać czy taka wersja jest ok 🙂

  2. Łukasz napisał(a):

    Tak jest super!!!

  3. Łukasz napisał(a):

    Mam taki kod. Co tu jest nie tak. Mozliwe że eclipce się zawiesza? Nie Pozwala utworzyć zmiennej public static final String STOP = "-";

    • Marcin Pietraszek napisał(a):

      Cześć Łukasz 🙂

      Twój kod się nie kompiluje, Eclipse w okienku „errors” powinien Ci o tym powiedzieć wskazując liniję gdzie definiujesz STOP.

      Nie odpowiem Ci wprost a zadam pytanie, zastanów się czym różnie się zmienna lokalna od atrybutu instancji klasy. Jakie modyfikatory/słowa kluczowe można użyć w przypadku atrybutu a jakie w przypadku zmiennej lokalnej?

  4. Łukasz napisał(a):

    Pole obiektu oznaczone jako final należy inicjowac poprzez jawne inicjowanie w definicji klasy.
    Zmienna lokalna widoczna jest tylko w obrebie metody i usuwana po jej wykonaniu natomiast zmienna static final nie moze byc modyfikowana

    • Marcin Pietraszek napisał(a):

      „Pole obiektu oznaczone jako final należy inicjowac poprzez jawne inicjowanie w definicji klasy.” – to połowa prawdy 😉 Takie atrybuty możesz także inicjalizować wewnątrz konstruktora obiektu.

      static – możemy stosować wyłącznie do atrybutów, nie można tego słowa kluczowego używać przed zmiennymi lokalnymi,
      final – możemy używać zarówno przed atrybutami jak i zmiennymi lokalnymi.

      Jak wygląda sprawa z public?

  5. Łukasz napisał(a):

    Public można tylko stosować do atrybutów klasy?

    • Marcin Pietraszek napisał(a):

      Modyfikatory dostępu typu public, private czy protected nie mogą być używane w przypadku zmiennych lokalny. Proszę czytaj dokładniej błędy kompilacji, aby myśleć poważniej o programowaniu po prostu musisz je dobrze odczytywać.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *