QuickSort Implementacja

Koniec wakacyjnej przerwy! Ten artykuł miał zostać opublikowany ponad tydzień temu – ale wiecie jak to jest z wakacjami 😉 czasem po prostu trzeb a zluzować troszkę! Ale wracam z naładowanymi bateriami! Dziś zajmiemy się implementacją QuickSortu!

01. QuickSort - Użycie wbudowanej implementacji / Bibloteki

Pod poprzednim artykułem, czytelnik zadał pytanie czy istnieje już biblioteka która posiad implementację quicksortu? – jak się można spodziewać, odpowiedź na to pytanie brzmi – oczywiście 😊 żeby było lepiej, implementacja QuickSortu znajduje się w samej Javie! 😊 (od wersji 1.2)

import java.util.Arrays;
...
Arrays.sort(array);

I w zasadzie to wystarczy 😊

02. QuickSort- Własna implementacja

Nasza własna implementacja nie jest najbardziej optymalną wersją – jest wersją przedstawionej wcześniej teorii. Opisana implementacja pokrywa się w 100% z opisaną teorią z poprzedniego wpisu, dzięki czemu można porównać co się dokładnie dzieje.

Po pierwsze mamy jeden punkt wejściowy do którego przesyłamy naszą nieposortowaną tablicę:

public static void sort(int[] array) {
  quickSort(array, 0, array.length-1);
}

W ciele metody robimy odesłanie do naszej głównej metody quickSort:

private static void quickSort(int[] array, int firstIndex, int lastIndex) {
  if(firstIndex < lastIndex) {
    int partitionIndex = partition(array, firstIndex, lastIndex);
    quickSort(array, firstIndex, partitionIndex-1);
    quickSort(array, partitionIndex+1, lastIndex);
  }
}

Pierwszym krokiem jest sprawdzenie na jakich pozycjach są ustawione indexy, index początkowy musi być mniejszy od ostatniego indexu – w przeciwnym wypadku nie podejmujemy żadnych działań.

Główną metodą jest metoda partition która dokonuje sortowania przesłanej tablicy względem pivotu, oraz zwraca również index podziału – dzięki czemu mamy podział na lewą i prawą część tablicy.

Jak możesz zauważyć metoda quickSort, jest metodą rekurencyjną (wywołuje sama siebie), pokrywa się to z sortowaniem lewej oraz prawej części tablicy 😊

Metoda partition wygląda następująco:

private static int partition(int[] array, int firstIndex, int lastIndex) {
  int pivot = array[lastIndex];
  int pIndex = firstIndex;

  for (int i = firstIndex; i < lastIndex; i++) {
    if(array[i] <= pivot) {
      swap(array, i ,pIndex);
      pIndex++;
    }
  }
  swap(array, pIndex, lastIndex);
  return pIndex;
}

Jest to metoda która sortuje względem wybranego pivotu, oraz zwraca index podziału. Metoda swap jest metodą pomocniczą która zamienia dwa wskazane elementy:

private static void swap(int[] array, int index1, int index2) {
  int tmp = array[index1];
  array[index1] = array[index2];
  array[index2] = tmp;
}

Podsumowanie

Metody sortowania mogą być kłopotliwe do zrozumienia, pamiętaj że cały kod dostępny jest na GitHubie! Ściągnij kod – poeksperymentuj z nim, pomoże Ci to w zrozumieniu tego co się dzieje!


QuickSort - Teoria

Kolejny wpis z cyklu JavaByExample. W poprzednim wpisie rozpisałem jak wygląda rekurencja – jeśli potrzebujesz to wróć do tego wpisu, dziś po raz kolejny użyjemy tej techniki!

01. QuickSort

Dzisiejsze zadanie jest kolejnym klasykiem – algorytmami sortującymi wykładowcy meczą studentów od zawsze, i pewnie już nigdy nie przestaną 😉

Zaimplementuj algorytm sortujący QuickSort

02. QuickSort - Teoria

Okej, jak wygląda ten algorytm? Powiem szczerze – jest naprawdę wiele wersji tego algorytmu. Jest ich tak wiele ze względu na proces optymalizacji jaką przechodził ten algorytm. My zajmiemy się dziś jedną z podstawowych wersji algorytmu QuickSort 😊

Ogólna założenia algorytmu: Wybierany jest element  według którego sortujemy (pivot), wszystkie elementy mniejsze, umieszczamy po jego lewej stronie a elementy większe – po jego prawej stronie.
Następnie powtarzamy tą czynność dla lewej i prawej strony, do momentu aż wszystkie elementy będą posortowane.

Daną wejściową jest nieposortowana tablica np.:

Pierwszym krokiem jest wybór elementu według którego będziemy sortować, potocznie ten element jest nazywany pivotem – dla uproszczenie można wybrać ostatni index tablicy wejściowej.

Zaczynamy nasze sortowanie. Iterujemy w pętli po elementach na pozycjach mniejszych niż nasz pivot. Będziemy potrzebować dodatkowego wskaźnika który będzie nam wskazywać na element z którym będziemy przestawiać elementy, nazwijmy sobie go pIndex.

Mamy nasze początkowe ustawienie. Sprawdzamy czy element na który wskazuje index pętli jest mniejszy od naszego pivot’u (4), jeśli tak zamieniamy element na który wskazuje index pętli z elementem na który wskazuje pIndex. Po zamianie zwiększamy pIndex.

W naszym przypadku 7 jest większy od wybranego pivotu (4) – nie podejmujemy żadnej akcji, przechodzimy w pętli do kolejnego elementu:

Czy element na który wskazuje index pętli (2) jest mniejszy od naszego pivotu (4) jest mniejszy? Tak! Zamieniamy zatem elementy znajdujące się na miejscach na który wskazuje index pętli oraz wskaźnik pIndex

Po zamianie zwiększamy pIndex:

Przestawiliśmy jeden element. Jedziemy dalej według tego samego schematu 😉

Przestawiamy elementy -> zwiększamy pIndex -> przchodzimy do kolejnego elementu w pętli

Przedstawiamy elementy -> zwiększamy pIndex -> index pętli nie zostanie zwiększony, doszliśmy do ostatniego sprawdzanego elementu:

W momencie gdy doszliśmy do ostatniego sprawdzanego elementu, dokonujemy dodatkowej zamiany, pIndexu z naszym pivotem:

Ufffff! Właśnie ustawiliśmy pierwszy element! Wszystkie elementy mniejsze od naszego wybranego pivotu znajdują się po jego lewej stronie, a wszystkie większe elementy po jego prawej stronie! Teraz to samo robimy z lewą stroną oraz prawą stroną 🙂

Podsumowanie

Ze względu że artykuł wyszedł dość obszerny, został podzielony na dwie części – teoria, dostępna w tym wpisie, oraz implementację – która zostanie omówiona w kolejnym wpisie. Jeśli chcesz sprawdzić implementację już teraz, jest ona dostępna na GitHubie!


Ciąg Fibonacciego

Dziś kolejne zadanie z cyklu JavaByExample. Dziś będziemy omawiać ciąg Fibonacciego. W poprzedniej lekcji z serii omawialiśmy przykład zliczania poszczególnych znaków z daniu. Dostałem na ten temat komentarz, iż zadania da się rozwiązać znacznie łatwiej niż zostało to przeze mnie przedstawione, używając Stremów. Bardzo dziękuję za ten komentarz! Poprzednie zadanie zostało zaktualizowane o rozwiązanie oparte na Streamach! Update znajdziecie we wpisie, jak i na GitHubie! 😊

01. Zadanie - Ciąg Fibonacciego

Dzisiejsze zadanie jest naprawdę klasykiem! 😊 Mała ciekawostka, kiedyś dostałem zadanie związane z ciągiem Fibonacciego na rozmowie rekrutacyjnej, jako zadnie na rozgrzewkę 😊 Warto zapoznać się o co chodzi 😊

Napisz program który policzy podany przez użytkownika element ciągu Fibonacciego.

Jak wygląda ten cały ciąg? Pierwsze dwie liczby to 1,1 a kolejna to suma dwóch poprzednich, czyli kolejnym elementem jest liczba 2! Mamy zatem 1,1,2 – kolejnym elementem jest 1+2 = 3 mamy teraz 1,1,2,3 – kolejny element 2+3=5 itd. Tak przedstawiają się kolejne elementy tego ciągu:

1,1,2,3,5,8,13,21,34,55,89… i tak w nieskończoność 😉

Przykład 1:
Użytkownik podaje liczbę: 6
Odpowiedź programu: 8

Przykład 2:
Użytkownik podaje liczbę: 10
Odpowiedź programu: 55

02. Uwaga!

Przede wszystkim – postaraj się rozwiązać to zadanie samodzielnie. Poniżej znajdziesz moje rozwiązanie, opis wraz z całym kodem źródłowym!

Przed rozpoczęciem mojego rozwiązania chciałbym przypomnieć o jednej bardzo ważnej sprawie. Pamiętaj o tym że nie ma jednego konkretnego sposobu na napisanie programu który rozwiązuje dany problem. Jak to powtarzał jeden z moich profesorów, konkretny program możemy napisać na milion sposobów i każdy z nich może być poprawny!

STOP -Poniżej znajdziesz moje rozwiązanie tego zadania – postaraj się rozwiązać najpierw samemu! W razie problemów możesz przeczytać moje rozwiązanie oraz zajrzeć na GitHuba!

03. Moje rozwiązanie

Zadanie można rozwiązać na wiele sposobów 😊 Naprawdę! Zadanie jest naprawdę klasyczne 😊 Wykładowcy lubią męczyć nim adeptów programowania, dziś przedstawię wam 3 różne sposoby rozwiązania, klasyczny z użyciem pętli, z użyciem rekurencji oraz z użyciem Stremów.

04. Fibonacci klasycznie

public static long fibClassic(int n) {
  long x1 = 1;
  long x2 = 1;
  for (int i = 3; i <= n; i++) {
    long temp = x1 + x2;
    x1 = x2;
    x2 = temp;
  }
  return x2;
}

Opis: Tworzymy dwie zmienne którym nadajemy wartość 1, zgodnie z pierwszymi elementami ciągu. Zaczynamy pętle, początek pętli ustalamy na 3, pierwsze dwa elementy są znane, wiec sens wykonywania jakichkolwiek obliczeń mamy dopiero od trzeciego elementu. W pętli tworzymy tymczasową zmienną do której przypisujemy wynik sumy dwóch poprzednich elementów. Następnie dokonujemy „przesunięcia” nasze x1 dostaje wartość obecnego x2, natomiast x2 dostaje wyliczoną wartość. Użytkownikowi zwracamy x2 😊 będzie on miał przypisaną ostatnią wyliczoną wartość 😊

05. Fibonacci Rekursywnie

public static long fibRecursive(int n) {
  if(n <= 1) return n;
  return fibRecursive(n-2)+fibRecursive(n-1);
}

Yeah, piękno rozwiązań rekursywnych 😉 Całe ciało metody zamknięte dosłownie w dwóch linijkach! Jeśli nigdy wcześniej nie spotkałeś się z metodami rekursywnymi już tłumaczę o co chodzi. W prostych słowach polega to na tym że metoda wywołuje sama siebie 😊 Metody rekursywne mają wiele pułapek, i łatwo stworzyć nieskończoną pętlę, trzeba ich używać ze szczególną ostrożnością.

Opis: Jeśli metoda dostanie liczbę 1 lub mniejszą, zwróci tą liczbę, jeśli dostanie liczbę większą od jeden zwraca sumę z wykonania metody dla dwóch poprzednich elementów (n-2 oraz n-1). Dla osób początkujących może być to ciężko do wyobrażenia więc spieszę z pomocnym rysunkiem! 😊 Żeby aż tak wiele nie rysować, przyjmiemy że użytkownik poprosił o 6 element ciągu (wynik 8) :

Okej! Rozrysowałem tylko jedną stronę 😉 ale myślę że z tego rysunku możecie sobie wyobrazić jak wygląda pozostała część 😊 Po prostu nie zmieściło mi się na rysunku 😊

Użytkownik podaje jako parametr wejściowy 6, zatem w zaczynamy naszą rekursywę fib(6-2)+fib(6-1). Czyli tak naprawdę fib(4)+fib(5). Rysunek przedstawia pełne drzewo dla fib(6-2) czyli dla fib(4), liczmy ostatnie gałęzie:

fib(4)= 0+1+1+0+1 = 3

Druga gałąź wyglądała by bardzo podobnie: fib(5)= fib(3)+fib(4) zwróć uwagę że fib(3) mamy policzone na rysunku i wynosi fib(3)=2, fib(4) również mamy policzone i wynosi fib(4)= 3, podstawiając liczby fib(5) = 2+3 = 5

Podstawiamy wszystkie policzone liczby fib(6) = fib(4) + fib(5) = 3+5 = 8

Uwaga! Rozwiązanie rekursywne jest bardzo wolne! Jest to spowodowane że niektóre wartości liczone są wielokrotnie.

06. Fibonacci Stream

public static long fibStream(int n) {
  return Stream.iterate(new int[]{1,1}, x -> new int[]{x[1], x[0]+x[1]})
      .limit(n)
      .map(x->x[0])
      .collect(Collectors.toList())
      .get(n-1);
}

Taka trochę ciekawostka że się po prostu da 😊 Nie uważam żeby to rozwiązanie było specjalnie atrakcyjne taka ciekawostka!

Podsumowanie

Ok! 3 całkowicie różne podejścia do rozwiązania danego problemu 🙂 Pamiętajcie że jeśli wasze może się różnić od mojego i nie ma w tym nic złego! Bonus! Na GitHubie dodałem porównanie wydajności poszczególnych metod, oraz jak Java radzi sobie z optymalizacją podobnych zadań 😉 fajnie to widać na Streamach!


Zliczanie znaków w zdaniu

Dziś oficjalnie rozpoczynamy serię Java By Example. Jestem przekonany że najlepszą nauką programowania jest po prostu praktyka. W serii Java By Example postawimy sobie jakiś problem który będziemy rozwiązywać, kody źródłowe będą dostępne oczywiście na GitHubie. Seria JavaByExample jest częścią kursu programowania w Javie.

01. Zadanie

Dzisiejsze zadanie jest zadaniem które pamiętam z podstaw programowanie:

Napisz program który wypisze liczbę występowania poszczególnych znaków, występujących we wprowadzonym przez użytkownika zdaniu.

Przykład 1:
Użytkownik wprowadza zdanie: ala
Odpowiedź programu: a -2 , l – 1

Przykład 2:
Użytkownik wprowadza zdanie: Ala ma kota
Odpowiedź programu: A – 1, a – 3, t – 1, k – 1, l – 1, m – 1, o – 1

Uwaga: Zwróć uwagę że wielka litera A jest liczona osobno od małej literki a

02. Uwaga!

Przede wszystkim – postaraj się rozwiązać to zadanie samodzielnie. Poniżej znajdziesz moje rozwiązanie, opis wraz z całym kodem źródłowym!

Przed rozpoczęciem mojego rozwiązania chciałbym przypomnieć o jednej bardzo ważnej sprawie. Pamiętaj o tym że nie ma jednego konkretnego sposobu na napisanie programu który rozwiązuje dany problem. Jak to powtarzał jeden z moich profesorów, konkretny program możemy napisać na milion sposobów i każdy z nich może być poprawny!

STOP -Poniżej znajdziesz moje rozwiązanie tego zadania – postaraj się rozwiązać najpierw samemu! W razie problemów możesz przeczytać moje rozwiązanie oraz zajrzeć na GitHuba!

03. Moje rozwiązanie

Ok – Czas pomyśleć nad rozwiązaniem. Zadanie możemy podzielić na 3 oddzielne elementy:

1) Pobranie od użytkownika wartości
2) Policzenie ile razy w zdaniu występuje konkretny znak
3) Wypisanie wyniku na ekranie

Podzielenie programu na osobne odpowiedzialności jest bardzo dobrą praktyką – każda część programu jest odpowiedzialna za tylko jedną funkcjonalność  – „Single responsibility principle”.

Ok! Zabieramy się za kodowanie. Pierwsza część programu jest prosta:

System.out.println("Input sentence: ");
Scanner scanner = new Scanner(System.in);
String userInput = scanner.nextLine();

Pobranie zdania od użytkownika mamy za sobą 🙂

Kolejna część jest tak naprawdę sercem naszego programu, musimy policzyć występowania poszczególnych znaków. Pobrane przez nas zdanie użytkownika jest przypisane do zmiennej typu String, żaden nasza metoda przyjmie String jako parametr. Metoda zwróci nam ma Mapę – jako klucz będzie znakiem, wartość liczbę która będzie reprezentować ile razy dany znak występował we wprowadzonym zdaniu.

public static Map<Character, Integer> countCharacters(String userInput) {
}

Cała metoda wygląda następująco:

public static Map<Character, Integer> countCharacters(String userInput) {
    if(userInput == null || userInput.isEmpty()) return Collections.emptyMap();

    Map<Character, Integer> characterCounter = new HashMap<>();
    for (char c : userInput.toCharArray()) {
        Integer value = characterCounter.get(c);
        if (value != null) {
            value++;
        } else {
            value = 1;
        }
        characterCounter.put(c,value);
    }

    return characterCounter;
}

Opis:
W pierwszym kroku sprawdzam co przyszło do metody, jeśli dostaliśmy nulla, lub pustego Stringa, nie ma sensu nic robić. Po prostu zwracamy pustą mapę. Jeśli jednak coś dostaliśmy od użytkownika do przetworzenia, mamy trochę roboty 😊

Tworzymy pustą mapę, oraz pętlę po wszystkich znakach z wprowadzonego zdania. W pętli staramy się pobrać z mapy wartość dla konkretnego znaku.

Jeśli dostaniemy wartość różną od null -> zwiększamy ją – znak wystąpił w zdani kolejny raz,  jeśli dostaniemy null, znaczy to że konkretnego znaku nie ma jeszcze w mapie – wartość ustawiamy na 1. W obu przypadkach dodajemy parę znak – ilość do mapy, w przypadku nowej wartości będziemy mieć kolejny element w mapie, w przypadku istniejącego już takiego klucza, zastąpimy go z nowa wartością.

Uwaga! Aktualizacja rozwiązania

Powyższa metoda w oparciu o pętlę, choć poprawna, to można ją zdecydowanie uprościć używając Streamów (Bardzo dziękuję czytelnikowi za zwrócenie uwagi!), streamy na blogu nie były jeszcze poruszane, ale warto zapoznać się również z tym rozwiązaniem:

public static Map<Character, Long> countCharactersStream(String userInput) {
  return userInput.chars().mapToObj(c -> (char) c)
      .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}

Metoda jest teraz niezwykle prosta! Cały wprowone zdanie zamieniamy w Stream, oraz zbieramy wszystkie unikalne elementy, oraz zliczamy ich występowanie używając

Collectors.groupingBy(Function.identity(), Collectors.counting())

Pozostaje nam jeszcze ostatnia część zadania – Wypisanie wyniku:

public static void printCountedInput(Map<Character, ? extends Number> characterCounter) {
   for (Map.Entry<Character, ? extends Number> entry : characterCounter.entrySet()) {
      System.out.println("Sign: "+entry.getKey().toString()+" Count: "+entry.getValue());
   }
}

Cały kod:

System.out.println("Input sentence: ");
Scanner scanner = new Scanner(System.in);
String userInput = scanner.nextLine();
printCountedInput(countCharacters(userInput));

Podsumowanie

Pamiętaj o punkcie drugim w którym wspominałem że nie ma jednego dobrego rozwiązania 😊 to jest tylko moja propozycja! Cały kod do zadania jest dostępny na Githubie! Możesz ściągnąć cały kod i uruchomić go u siebie 😊


Wyjątki - Podstawa

Kolejna część kursu programowania w Javie! Zanim rozpocznę dodatkową serię, roboczo nazwaną Java By Example, chciałbym omówić jeszcze jeden zdecydowanie ważny element języka jakim są wyjątki (ang. Exceptions). Wyjątki w Javie potrafią zaskoczyć. Powiem szczerze że niektóre decyzje projektowe zaskakują mnie do dziś na tym obszarze 😉

01. Java Exceptions

Wyjątki w Javie standardowo dzielimy na dwie grupy – Checked exceptions oraz Unchecked Exceptions. Różnica polega na tym że Checked exceptions muszą zostać obsłużone przez programistę, natomiast Unchecked Exceotions mogą być „pozostawione” same sobie. Tyle jeśli chodzi o prostą teorię.

Zawsze pomocny przy takich tematach jest mały diagram 😊 zatem proszę bardzo:

Małe wyjaśnienie. Wszystko co dziedziczy po klasie Throwable zaliczamy do wyjątków, mamy początkowe rozgałęzienie na Error oraz Exception.

Error w 99,9% nie powinien nas przejmować, w momencie kiedy nasz program sypnie errorem nie jesteśmy w stanie nic zrobić. Nasz program po prostu „padnie”. Musimy wtedy poszukać błędu w naszej logice może pojawiła się jakaś nieskończona pętla? Może dodajemy bardzo wiele elementów do ArrayListy co spowoduje przepełnienie pamięci? Tak czy siak, jeśli nasz program wyrzuci error, prawdopodobnie się wyłączy.

W drugiej gałęzi mamy klasę Exception, jest to pierwszy z Checked exceptions (pomijając Throwable który jest rodzicem dla wszystkich wyjątków 😉 ) – wyjątek który musi zostać obsłużony przez programistę. Poniżej mamy RuntimeException który jest pierwszym z Unchecked excptions – wyjątków które nie wymagają od programisty żadnych akcji 😊

02. Try - Catch - Finally

Podstawowym mechanizmem jest blok try catch finally, z czego blok finally jest opcjonalny, jest to blok kodu który zawsze się wykona, bez znaczenia czy wystąpi wyjątek czy nie.

try {
    //code
} catch (Exception ex) {
    //code
} finally {
    //code 
}

03. Checked Exceptions

Checked Exceptions – na diagramie są zaznaczone kolorem czerwonym. Nie są to wszystkie wyjątki Checked Exception’y! Nie starczyło by mi nocy żeby wyrysować je wszystkie 😉 Tak jak zostało to wcześniej powiedziona, są to wyjątki które muszą zostać obsłużone przez programistę.

Jednym z najbardziej popularnych tego typu wyjątków to IOException:

String pathToFile = "C:\\Users\\abc.txt";
BufferedReader bufferedReader = null;
try {
    bufferedReader = new BufferedReader(new FileReader(pathToFile));
    System.out.println(bufferedReader.readLine());
} catch (IOException e) {
    e.printStackTrace();
    System.out.println(e.getMessage());
} finally {
    if(bufferedReader != null) {
        try {
            bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    System.out.println("blok finally wykona sie zawsze :)");
}

Co tu się dzieje? Po prostu próbujemy odczytać plik znajdujący się na dysku naszego komputera. Wiele rzeczy przy takiej operacji może pójść nie tak! Chociażby – nie ma takiego pliku w określonej ścieżce.

W momencie kiedy próbujemy użyć metody która w swojej sygnaturze mówi o tym iż może rzucić takim wyjątkiem, nasze IDE podpowie nam, iż musimy taki wyjątek obsłużyć.

Np. metoda readLine()

public String readLine() throws IOException

Aby użyć takiej metody możemy ją zamknąć w bloku try catch tak jak zostało to przedstawione powyżej, lub przesłać wyjątek „dalej do góry”

public static void main(String[] args) throws IOException {
    String pathToFile = "C:\\Users\\abc.txt";
    BufferedReader bufferedReader = null;
    bufferedReader = new BufferedReader(new FileReader(pathToFile));
    System.out.println(bufferedReader.readLine());
}

04. Unchecked Exceptions

Unchecked Exception – na diagramie oznaczone kolorem zielonym, są to wyjątki które nie wymagają od programisty żadnej reakcji.  Przykład:

String param = "abc";
Integer number = Integer.valueOf(param);

Jednak oczywiście jeśli mamy takie życzenie, możemy obsłużyć taki wyjątek:

String param = "abc";
try {
    Integer number = Integer.valueOf(param);
    System.out.println("Udała się konwersja na liczbę :)");
} catch (NumberFormatException e) {
    System.out.println("Ejjj '"+param+"' to nie jest liczba!");
}

 

Nieprzypadkowo wybrałem tego typu przykład 😉 Na StackOverflow jest bardzo fajny temat w którym ludzie dyskutują na temat czy np. NumberFormatException powinien być typu checked (powinna być wymagana obsługa), no ale polecam zajrzeć już do tego wpisu. Naprawdę bardzo ciekawa lektura!

05. Podsumowanie

Temat wyjątków został jedynie zarysowany w tym wpisie 😊 Jednak jest to solidna podstawa która w połączeniu z resztą kursu pozwoli nam pisać podstawowe aplikacje! Koncepcje wyjątków w Javie jeszcze poruszę w bardziej zaawansowanym stopniu, w późniejszej części kursu!


Kolekcje - Mapy

Dotarliśmy do końca drogi kolekcji, dziś omówimy Mapy, które zamkną nam serię dotyczącą podstaw kolekcji. Jak to zostało wspomniane we wstępie serii, mapy nie są ściśle kolekcjami – nie dziedziczą interfejsu Collection, jednak zawsze gdy mówi się o kolekcjach w Javie, mapy dokłada do całego grona 😊

01. Mapy

Mapy są specyficzną strukturą danych, które przechowują elementy w sposób klucz – wartość. Oczywiście Mapy są generyczne – o typie kluczy jak i wartości decyduje programista, jest to kolejna cecha łącząca Mapy z resztą kolekcji.

Mapa jest bardzo przydatną strukturą, pozwala bardzo szybko pobrać z niej elementy, pod warunkiem że znasz odpowiedni klucz 😉

Klucze w Mapie są unikalne, jednak wartości w mapach mogą się powtarzać. W zależności od implementacji (HashMap oraz LinkedHashMap) możemy posiadać nulle jako klucz oraz wartość, natomiast w przypadku TreeMapy nulle są niedozwolone.

02. Implementacje

HashMap – Jest najpopularniejszą implementacją, podobnie jak HashSet, HashMapa nie zachowuje konkretnej kolejności.

Map<Integer, String> map = new HashMap<>();
map.put(10, "Arek");
map.put(2,"Zuza");

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

Wynik:

Zuza
Arek

LinkedHashMap – Jest analogiczna do LinkedHashSetu, zachowuje kolejność elementów w jakiej zostały one dodane.

Map<Integer, String> map = new LinkedHashMap<>();
map.put(10, "Arek");
map.put(2,"Zuza");

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

Wynik:

Arek
Zuza

TreeMap – Jest analogiczna do TreeSetu, wartość są posortowane według comparatora klucza. Możemy podać własny comparator podczas tworzenia mapy. Jeśli tego nie zrobimy, zostanie użyty domyślny comparator klucza.

Map<Integer, String> map = new TreeMap<>();
map.put(10, "Arek");
map.put(2,"Zuza");
map.put(8,"Klaudiusz");

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

Wynik:

Zuza
Klaudiusz
Arek

03. Podstawowe operacje

Podstawowymi operacjami na Mapach są operacje put oraz get. Metoda put dodaje element do mapy w postaci klucz – wartość:

Map<Integer, String> map = new HashMap<>();
map.put(10, "Arek");
map.put(2,"Zuza");
map.put(8,"Klaudiusz");

Natomiast metoda get pozwala nam wyciągnąć daną wartość po konkretnym kluczu.

String value = map.get(2);

Jeśli chcemy przeszukać całą mapę, możemy pobrać wszystkie klucze jako Set:

Set<Integer> keySet = map.keySet();

wartości jako Collection:

Collection<String> values = map.values();

lub wszystkie elementy mapy w postaci klucz – wartość jako Set 😊

Set<Map.Entry<Integer, String>> entries = map.entrySet();

Dzięki takim pomocniczym metodą, możemy w łatwy sposób iterować po interesujących nas elementach, korzystamy przecież z dobrze nam znanych kolekcji 😊

04. Podsumowanie

Mapy są specyficzną strukturą danych. Umiejętne posługiwanie się mapami pozwoli oszczędzić nam naprawdę sporo mocy obliczeniowej w momencie gdy musimy wyszukiwać specyficzne elementy, dobrze przemyślana mapa może zdecydowanie przyspieszyć działanie naszego programu 😊


Kolekcje - Kolejki

Kontynuacja tematu kolekcji 😊 Tak ja powiedziałem w pierwszym wpisie z serii, temat kolekcji jest dość złożony. Dziś omówimy kolejki. Muszę przyznać że przez ponad 7 lat aktywnego programowania czystych Javowych kolejek nie używałem zbyt często 😉 Jednak nie zmienia to faktu że kolejki w określonych sytuacjach mogą być naprawdę bardzo przydatne!

01. Kolejki

Kolejki zachowują się mniej więcej jak kolejki w sklepie. Ktoś podchodzi do kasy, zanim ustawiają się kolejne osoby, pierwsza osoba zostaje obsłużona, odchodzi o kasy, kolejna osoba jest z przodu, gotowa aby ją obsłużyć. Większość kolejek w Javie jest typu FIFO – first in first out, dokładnie takich jak w sklepie.

Odstępem od kolejek typu FIFO jest PriorityQueue o której wspomniałem w pierwszym wpisie odnośnie kolekcji. Kolejka ta zachowuje się podobnie do TreeSetu, sortuje elementy według comperatora.

ArrayDeque jest  specyficzną implementacją kolejki która pozwala na obsługę elementów z początku jak i końca kolejki.

Ciekawostką jest to że LinkedList jest jednocześnie Listą oraz Kolejką 😊 (Spójrz na rysunek w pierwszym wpisie o kolekcjach), zatem całkowicie poprawnym sposobem na utworzenie kolejki jest taki zapis:

Queue<String> stringQueue = new LinkedList<>();

02. Podstawowe operacje

Podstawowe operacja dla kolejek add, offer, remove, poll, element oraz peek. Wszystkie te metody są zadeklarowane w interfejsie Queue. Jednak tylko trzy z tych metod są najczęściej wykorzystywane, są to:

offer – dodaje element do kolejki, jest to preferowana metoda dodawania elementu do kolejki, metoda add w niektórych implementacjach podczas nieudanego dodania rzuca wyjątkiem, co może zaskoczyć 😉 Gdy pracujemy z kolejkami i chcemy dodać do niej element, stosujmy offer.

poll – pobiera oraz zdejmuje element z początku kolejki.

peek – pobiera element z kolejki bez zdejmowania go.

Należy pamiętać że kolejki należą do kolekcji! Możemy zatem sprawdzić ile elementów znajduje się w kolejce wywołując metodę size, czy przejść przez wszystkie elementy z wykorzystaniem pętli.

Queue<String> stringQueue = new LinkedList<>();
stringQueue.offer("Arek");
stringQueue.offer("Karol");
stringQueue.offer("Robert");
stringQueue.offer("Szymon");

System.out.println(stringQueue.size());
System.out.println(stringQueue.poll());
System.out.println(stringQueue.size());
System.out.println("--------");

for (String hero : stringQueue) {
    System.out.println(hero);
}

Podsumowanie

Kolejki są naprawdę specyficznym rodzajem kolekcji, jest to bardzo przydatna struktura w określonych sytuacjach. Dobrze mieć świadomość tego jak działa i jakie możliwości oferuje. Nigdy nie wiadomo kiedy przyjdzie czas w którym może uratować sytuację 😊


Kolekcje - Listy

Dziś kontynuujemy temat kolekcji. W poprzednim wpisie zostały omówione Sety, dziś natomiast zajmiemy się Listami. Temat będzie nieco krótszy od Setów (uffff!), chociażby ze względu na to że omówimy tylko najbardziej popularne implementacje, ArrayList oraz LinkedList. Zapraszam! 🙂

01. Lista a Set - podobieństwa i różnice

Niezależnie od implementacji, Sety oraz Listy zaliczamy do grona kolekcji, wywodzą się od wspólnego interfejsu Collection. Listy tak samo jak Sety, są generyczne. Czym są generyki zarysowałem w poprzednim wpisie odnośnie Setów.

Główną różnicą jest to iż Lista pozwala przechowywać duplikaty, dokładnie ten sam obiekt, lub taki sam obiekt, możemy dodać wielokrotnie do Listy, gdzie Set nam to uniemożliwi.

Wiele stron oraz kursów w różnicach podaje że Lista jest uporządkowana, natomiast Set nie. Nie jest to prawda! Wszystko zależy od użytej implementacji Setu np. LinkedHashSet zachowuje kolejność dodania 😊

02. Listy

Niezależnie od implementacji z jakiej korzystamy, dysponujemy takim samym podstawowym arsenałem funkcjonalności. Możemy dodawać pojedyncze elementy do listy, dodawać całe kolekcje, usuwać elementy, sortować,  iterować po wszystkich elementach, lub pobrać konkretny element z listy.

Nie będę omawiać tutaj każdego elementu z osobna, operacje są tak domyślne że zachęcam was do samodzielnego eksperymentowania! 😊

Główna różnica leży w wydajności konkretnych implementacji dla danej operacji, np. dodawanie wielu elementów do ArrayListy jest niewydajne, do tego zdecydowanie lepiej nadaje się LinkedLista.

List<Hero> heroList = new LinkedList<>();
heroList.add(new Person("Arek"));
heroList.add(new Person("Arek"));
heroList.add(new Person("Dominika"));
heroList.add(new Person("Karolina"));
heroList.add(new SuperPerson("Dominika"));
for (Hero hero : heroList) {
    System.out.println(hero);
}

//skorzystałem z tych samych klas co w omówieniu Setów

03. ArrayList

ArrayList jest tak naprawdę dynamiczną tablicą. W ramach przypomnienia odsyłam do lekcji o tablicach, zauważ że podczas tworzenia tablicy deklarujemy jej wielkość która jest niezmienna.

ArrayList wewnętrznie używa tablicy do przechowywania danych, a w momencie dodawania sprawdza czy zbliżamy się do końca tablicy, jeśli tak, tworzona jest nowa tablica o większym rozmiarze, i przepisywane są do niej elementy ze starej tablicy.

Jeśli będziemy dodawać bardzo dużo elementów do ArrayListy, możemy mieć problemy związane z wydajnością. ArrayList ma natomiast przewagę jeśli chcemy odczytywać elementy z wybranych indexów. Dostęp do każdego elementu jest stały.

04. LinkedList

LinkedList wewnętrznie przechowuje elementy w podwójnie związanej liście elementów. Każdy z elementów posiada wskaźnik do następnego jak i poprzedniego elementu. Iterowania po wszystkich elementach jest szybkie, natomiast pobranie konkretnego elementu z danego indexu wymaga większego nakładu pracy. Wewnętrznie i tak musimy przejść przez wszystkie poprzedzające elementy aż dojdziemy do konkretnego indexu.

Zaletą LinkedListy w porównaniu z ArrayListą jest szybkość wykonywania operacji dodawania oraz usuwania.

Podsumowanie

LinkedLista powinna być wybierana w przypadku gdy nie posiadamy „losowych” pobrań elementów (z konkretnego indexu), oraz gdy na danej liście stosujemy wiele operacji dodawania lub usuwania elementów.

Jeśli zaś liczba elementów w liście jest stała, oraz potrzebujemy bardzo szybkiego dostępu do elementów znajdujących się na konkretnych pozycjach, wtedy zdecydowanie lepszym wyborem będzie ArrayList.


Kolekcje - Sety

W poprzednim wpisie omówiliśmy sobie jakie mamy kolekcje, oraz w telegraficznym skrócie przedstawiłem najpopularniejsze implementacje. W tym wpisie omówimy sobie temat troszkę bardziej szczegółowo na prostych przykładach! Na pierwszy ogień idą sety! Zapraszam do lektury! 😊

01. Kolekcje są generyczne!

Pierwszą sprawa jaką musimy sobie powiedzieć to że kolekcje są generyczne. Nie przejmuj się jeśli zupełnie nie wiesz o co chodzi, już tłumaczę. W prostych słowach można powiedzieć że kolekcje przechowują jeden określony typ danych, podobnie jak tablice. Podczas deklaracji określamy jakiego typu będą elementy 😊

Set<String> stringList = new HashSet<>();
Set<Integer> integerList = new LinkedHashSet<>();
Set<Person> personList = new TreeSet<>();

Powyższy przykład przedstawia deklarację oraz stworzenie setów, pierwsza może przechowywać Stringi, drugi liczby całkowite, natomiast trzeci naszą klasę którą nazwałem Person. Ogólny zapis to Set<E> gdzie E określa nam dowolny element.

02. HashSet

Okej! Tak jak w poprzednim wpisie zaczynamy od Setów. Podstawową, oraz najczęściej wykorzystywaną implementacją jest HashSet. Jego właściwości omówimy sobie na prostych przykładach. Po pierwsze utworzymy sobie bardzo prostą klasę Person:

public class Person  {

    private String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public String toString() {
        return name;
    }
    
}

Kod dla metody equals oraz hashCode został wygenerowany! Odsyłam do wpisu o equals oraz hashCode gdzie wspominałem o tym aby podczas nadpisywania tych metod korzystać z pomocy naszego IDE!

Zabieramy się za prosty przykład, tworzymy kilka obiektów, oraz dodajemy je do HashSetu:

Person person1 = new Person("Arek");
Person person2 = new Person("Konrad");
Person person3 = new Person("Amigo");

Set<Person> personSet = new HashSet<>();
personSet.add(person1);
personSet.add(person2);
personSet.add(person3);

for (Person person : personSet) {
    System.out.println(person);
}

Zauważ że wynik na konsoli może różnić się od tego w jakiej kolejności elementy zostały dodane. W moim przypadku wynik jest następujący:

Konrad
Amigo
Arek

Tak jak zostało to powiedziane w poprzednim wpisie, HashSet nie zapewnia nam specyficznej kolejności, przechowuje elementy według własnej wewnętrznej implementacji bazując na metodzie hashCode.

O HashSecie możemy myśleć jak o worku do którego rzucamy elementy tego samego typu. Wrzucamy wszystko do jednego wora, ale nie wiemy w jakiej kolejności będziemy z tego wora wyciągać.

W poprzednim wpisie powiedziałem o tym że Set nie może przechować duplikatów, rozważmy taki przykład:

Set<Person> personSet = new HashSet<>();
personSet.add(person1);
personSet.add(person1);
personSet.add(person2);
personSet.add(person3);

for (Person person : personSet) {
    System.out.println(person);
}

Zauważ że obiekt person1 (Arek) został dodany dwukrotnie do Setu, a oto co się stanie w momencie kiedy wpiszemy jego elementy na konsolę:

Konrad
Amigo
Arek

Arek – został wypisany tylko raz, stało się tak dlatego że próbowaliśmy dodać dwukrotnie ten sam obiekt do setu, drugi obiekt nie został dodany. Cała sekwencja wyglądała następująco: podczas dodawania elementu do Setu, pobierany jest hashCode elementu, Set widzi że już posiada taki hashCode, wywołuje wówczas metodę equals aby sprawdzić czy obiekty są takie same. Jeśli equals zwróci true, obiekt nie jest dodawany do Setu.

Okej, ale co w przypadku gdy dwa różne obiekty posiadają taki sam hashCode? Tak swoją drogą jest to częste pytanie na rozmowach rekrutacyjnych, czy można dodać dwa obiekty do hashSetu o takim samym hashCodzie? Można! Pod warunkiem że equals dla tych obiektów zwraca false! 😊

Małe ostrzeżenie! Nie wystarczy stworzyć nowy obiekt person i nadać mu takie samo imię (np. Arek). Przeanalizujmy sobie ten przykład. Tworzymy nowy obiekt person (person4), dajemy mu imię Arek oraz dodajemy do setu:

Person person1 = new Person("Arek");
Person person2 = new Person("Konrad");
Person person3 = new Person("Amigo");
Person person4 = new Person("Arek");

Set<Person> personSet = new HashSet<>();
personSet.add(person1);
personSet.add(person1);
personSet.add(person2);
personSet.add(person3);
personSet.add(person4);

for (Person person : personSet) {
    System.out.println(person);
}
Konrad
Amigo
Arek

Na konsoli ciągle widzimy tylko 3 unikalne imiona! W skrócie przypomnę że equals sprawdza czy obiekty są „takie same.” Więc dla dwóch różnych obiektów klasy Person o takich samym imionach metoda equals zwróci true. Po więcej odsyłam do lekcji na temat equals oraz hashCode.

Dobrze! Jak zatem osiągnąć stan w którym w HashSecie posiadamy dwa obiekty o takim samym hashCodzie?  😉 Całkiem prosto! Utwórzmy sobie pusty interfejs, nazwijmy go Hero 😉 oraz zaimplementujmy go w naszej klasie Person:

public interface Hero {}
public class Person implements Hero

Na pierwszy rzut oka nic się nie zmieniło prawda? Okej! Tworzymy nową klasę np. SuperPerson, która jest praktycznie taka sama jak klasa Person, posiada tylko pole o nazwie imię. Nasza nowa klasa SuperPerson również implementuje interfejs Hero.

public class SuperPerson implements Hero {

    private String name;

    public SuperPerson(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        SuperPerson superPerson = (SuperPerson) o;
        return Objects.equals(name, superPerson.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public String toString() {
        return name;
    }
}

Teraz możemy utworzyć HashSet który będzie przechowywać elemnty Hero!

Person person = new Person("Arek");
SuperPerson superPerson = new SuperPerson("Arek");

Set<Hero> heroSet = new HashSet<>();
heroSet.add(person);
heroSet.add(superPerson);

for (Hero hero : heroSet) {
    System.out.println(hero);
}

Mamy tutaj przykład w którym dwa różne obiekty (wywołanie dla nich metody equals zwróci false), posiadają taki sam hashCode są w tym samym Secie 😊

03. LinkedHashSet

Główną różnicą pomiędzy LinkedHashSetem a HashSetem jest zachowanie kolejności podczas dodawania do Setu. LinkedHashSet przechowuje elementy w kolejności ich dodania. Powróćmy do pierwszego przykładu, z wyjątkiem iż zamiast HahsSetu utworzymy LinkedHashSet

Person person1 = new Person("Arek");
Person person2 = new Person("Konrad");
Person person3 = new Person("Amigo");

Set<Person> personSet = new LinkedHashSet<>();
personSet.add(person1);
personSet.add(person2);
personSet.add(person3);

for (Person person : personSet) {
    System.out.println(person);
}

Elementy na konsoli zostaną wypisane w kolejności w jakiej zostały dodane do LinkedHashSet.

Arek
Konrad
Amigo

04. TreeSet

TreeSet zapewnia nam że elementy będą posortowane w odpowiedni sposób. Elementy w TreeSecie powinny implementować interfejs Comparable, lub powinniśmy podać podczas tworzenia TreeSetu comperator według którego obiekty będą sortowane. Dodajemy do klasy Person interfejs Comperable:

public class Person implements Hero, Comparable<Person> {

    private String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public String toString() {
        return name;
    }

    @Override
    public int compareTo(Person o) {
        return this.name.compareTo(o.getName());
    }
}

Nasz klasa implementuje teraz interfejs Comparable, wraz z odpowiednią metodą compareTo. Dodaliśmy również getter dla pola name.

Oraz tworzymy TreeSet:

Set<Person> personTreeSet = new TreeSet<>();
personTreeSet.add(person1);
personTreeSet.add(person2);
personTreeSet.add(person3);

for (Person person : personTreeSet) {
    System.out.println(person);
}
Amigo
Arek
Konrad

Podsumowanie

Uf! Sety mamy za sobą! Chociaż jeśli miałbym być szczery to i tak wierzchołek góry lodowej. Jednak tematy które zostały tu opisane dają solidną podstawę aby temat explorować dalej samemu, do czego serdecznie zachęcam!


Java Kolekcje

W momencie kiedy mamy już omówione dwa bardzo ważne koncepty, interfejsy oraz klasy abstrakcyjne, czas przejść do kolejnej niezmiernie istotnej części. Dziś porozmawiamy o kolekcjach w Javie.

01. O co właściwie chodzi?

Kolekcje w Javie są strukturą danych (podobnie jak tablice!), służą do przechowywania, oraz manipulowania grupą obiektów. To tyle w telegraficznym skrócie, jednak ten temat jest naprawdę ogromny oraz niezmiernie ważny! Z kolekcjami będziesz mieć do czynienia praktycznie cały czas! Dlatego jest niezmiernie ważne aby ten temat dobrze opanować.

02. Jakie posiadamy kolekcje w Javie?

W Javie istnieje interfejs o nazwie Collection (jak by inaczej prawda 😉 ?) Rozszerzają go kolejne interfejsy List, Set oraz Queue , te interfejsy stanowią podstawę kolekcji w Javie. Oczywiście to nie wszystko! Istnieją jeszcze inne interfejsy oraz sporo klas które je implementują.

Ciężko opisać wszystkie zależności, zdecydowanie  jedne obrazek zastępuje więcej niż tysiąc słów!

Powyższy schemat nie przedstawia wszystkiego! W skład wchodzi jeszcze wiele klas abstrakcyjnych, rozrysowanie wszystkiego zajęło by naprawdę wiele czasu 😉 Nie o to jednak chodzi! Schemat przedstawia całkiem porządnie jakie mamy kolekcje:

  • Sety
  • Listy
  • Kolejki

Warto wspomnieć jeszcze o mapach. Mapy są strukturą danych która przechowuje elementy w sposób klucz – wartość. Jeśli bardzo restrykcyjnie podejść do tematu, mapy nie zaliczamy do kolekcji. Mapa nie implementuje interfejsu Collection, jednak bardzo często w kontekście rozmów o kolekcjach w Javie mówi się również o mapach (ze względu na to iż jest to struktura danych 😉 )

/**

Tekst będzie zawierać bardzo dużo informacji! Tak jak wspomniałem na początku, jest to bardzo obszerny temat. W tym wpisie przedstawię ogólny zarys związany z każdym elementem. W kolejnych wpisach zajmiemy się każdym elementem osobno! Malutkimi kroczkami!

**/

03. Sety

Set definiuje unikalny zbiór elementów, wykorzystuje do tego metodę equals . W prostych słowach Set nie może przechowywać dwóch takich samych obiektów (takich dla których equals jest true)

Elementy w Secie nie posiadają indexu pod którym można by odnieść się do konkretnego elementu (mam na myśli tutaj sytuację jak w tablicy gdzie możemy odnieść się do konkretnego indexu), dostęp do elementów odbywa się za pomocą iteratora.

Podstawową implementacją jest HashSet, który jest używany w większości przypadków. HashSet wykorzystuje metodę hashCode aby określić jak dany obiekt jest przechowywany. Nie mamy w tym przypadku wpływu na to jak elementy są przechowywane, element dodany jako któryś z kolei, podczas iterowania może być pierwszy!

LinkedHashSet zapewnia nam że elementy będą przechowywane w kolejności jakiej zostaną dodane.

TreeSet zapewnia nam że dodane elementy będą posortowane. W jaki sposób będą posortowane? Obiekty trzymane w TreeSecie powinny implementować interfejs Comparable który posiada tylko jedną metodę compareTo która mówi o tym czy dany obiekt jest większy, równy czy mniejszy od innego. Jeśli chcemy przechować w TreeSecie obiekty które nie implementują tego interfejsu, podczas tworzenia TreeSetu musimy podać w jaki sposób obiekty będą porównywane.

04. Listy

Lista w odróżnieniu od setów może przechowywać duplikaty (dokładnie takie same obiekty), kolejność przechowywania elementów zależy od ich dodania do listy.

Podstawową implementacją jest ArrayList, pod spodem leży tak naprawdę tablica 😊 Drugą popularną implementacją jest LinkedList, różnica pomiędzy tymi dwiema implementacjami jest w wydajności podczas określonych operacji.

O szczegółach porozmawiamy w osobnym wpisie 😉

05. Kolejki (Queue)

Kolejka wygląda mniej więcej tak jak w sklepie do kasy 😉 Osoby się ustawiają w kolejce, pierwsza osoba jest obsługiwana, następnie przechodzimy do kolejnej osoby.

Jest to pewne uproszczenie tematu ponieważ z wykorzystaniem konkretnej implementacji (ArrayDeque) możemy obsługiwać z początku jak i końca kolejki.

PriorityQueue działa bardzo podobnie jak TreeSet, wymagany jest komperator i według niego elementy są przechowywane w kolejce. Różnicą jest że ta kolejka może przechowywać duplikaty!

06. Mapy

Mapa jest strukturą która przechowuje elementy w sposób klucz – wartość. Podstawową implementacją jest HashMap.

Podsumowanie

W tym wpisie nie będę wchodzić w szczegóły. Gdybym chciał zrobić to w jednym wpisie byłby on zdecydowanie za długi. W kolejnych wpisach skoncentruje się na poszczególnych elementach. Setach, Listach, Kolejkach oraz Mapach. W Każdym wpisie omówię najważniejsze koncepcje związane z danym elementem. Czeka nas całkiem długa podróż! 😉