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óż! 😉


Klasy Abstrakcyjne

Kolejny wpis poświęcony programowaniu w Javie! Fajnie widzieć gdy strona kursu programowania wypełnia się kolejnymi lekcjami! W Poprzednich dwóch wpisach, przedstawiłem wam o co chodzi z tymi interfejsami w Javie, jak można je wykorzystać oraz dlaczego są tak ważne. Dziś porozmawiamy o kolejnym niezwykle istotnym elemencie jakim są klasy abstrakcyjne!

01. Klasa abstrakcyjna

Zanim przejdziemy do klas abstrakcyjnych, małe przypomnienie o jednym z pierwszych postów! O zwykłych klasach rozmawialiśmy w TYM poście, warto sobie przypomnieć czym właściwie jest klasa, oraz odświeżyć sobie jaka jest różnica między klasą a obiektem 😊 Wszystko znajdziecie w lekcji Pakiety, klasy, obiekty oraz podstawowe typy danych.

Klasa abstrakcyjna, różni się od zwykłej klasy już na poziomie jej deklaracji, występuje tutaj słowo kluczowe abstract. Prosty przykładzik:

public abstract class SimpleAbstract {
    
}

Jak wcześniej napisałem, słowo abstract w deklaracji klasy mówi nam o tym że mamy do czynienia z klasą abstrakcyjną. Jednak w tym przypadku nasza klasa abstrakcyjna nie dostarcza nam żadnej funkcjonalności, ale spokojnie, jeszcze do tego dojdziemy 😊

02. Podstawowe informacje o klasie abstrakcyjnej

Jak wcześnie zostało to już powiedziane, klasa abstrakcyjna posiada w swojej deklaracji słowo abstracrt. To czyni ją klasą abstrakcyjną. Najważniejszą sprawą jest że klasa abstrakcyjna musi być rozszerzona, a jej abstrakcyjne metody zaimplementowane (no, to jest troszeczkę naciągnięcie, ale za chwilkę sprostuje tę wypowiedź).

Pamiętacie lekcje z interfejsami (Podstawy, Część druga)? Pojawiło się tam słówko kluczowe implements, w kontekście rozmowy o klasach abstrakcyjnych mówimy o rozszerzaniu klasy. Robimy to przy użyciu słowa extends.

Dodajmy do naszej  prostej klasy abstrakcyjnej prostą metodę:

public abstract class SimpleAbstract {

    public void run() {
        System.out.println("SimpleAbstract run");
    }
}

Oraz tworzymy prostą klasę która będzie rozszerzać naszą abstrakcję:

public class SimpleClass extends SimpleAbstract {
    
}

W tym momencie możemy użyć naszej klasy:

SimpleClass simpleClass = new SimpleClass();
simpleClass.run();

Czas na małe sprostowanie, wcześniej napisałem że klasa abstrakcyjna musi zostać rozszerzona, nie jest to do końca prawda, Java pozwala nam utworzyć instancje klasy anonimowej, jednak podczas tworzenia obiektu, musimy zapewnić implementacje wszystkich metod.

SimpleAbstract simpleAbstract = new SimpleAbstract() {
    @Override
    public void run() {
        //Implementation
    }
};

W sytuacji kiedy mamy tylko jedną metodę, możemy się jeszcze o taki zapis pokusić, jednak staje się to mocno niepraktyczne w momencie kiedy klasa abstrakcyjna posiada troszkę większą logikę 😉

03. Metody abstrakcyjne

Przede wszystkim należy wspomnieć o metodach abstrakcyjnych. Metody abstrakcyjne zostały już omówione w poprzedni lekcjach o interfejsach (Część pierwsza, Część druga). Metody abstrakcyjne w klasach abstrakcyjnych posiadają słowo kluczowe abstract, i muszą posiadać implementację w klasach które rozszerzają klasę abstrakcyjną.

Dołóżmy do naszej klasy abstrakcyjnej, jedną abstrakcyjną metodę:

public abstract class SimpleAbstract {

    public void run() {
        System.out.println("SimpleAbstract run");
    }
    
    public abstract void someMethod();
}

W tym momencie klasa która ją rozszerza musi zaimplementować abstrakcyjną metodę:

public class SimpleClass extends SimpleAbstract {

    @Override
    public void someMethod() {
        System.out.println("Lets code!");
    }
}

04. Konstruktor

Ciekawie wygląda sytuacja, w momencie kiedy przyjrzymy się jaka jest hierarchia tworzenia danego obiektu. Aby uzmysłowić sobie ten proces, w obu klasach dodamy konstruktor który wypisze nam na konsolę kiedy został wywołany.

public abstract class SimpleAbstract {

    public SimpleAbstract() {
        System.out.println("SimpleAbstract constructor");
    }

    public void run() {
        System.out.println("SimpleAbstract run");
    }
    
    public abstract void someMethod();
}
public class SimpleClass extends SimpleAbstract {

    public SimpleClass() {
        System.out.println("SimpleClass constructor");
    }

    @Override
    public void someMethod() {
        System.out.println("Lets code!");
    }
}

Tworzymy nasz obiekt w taki sam sposób jak zazwyczaj:

SimpleClass simpleClass = new SimpleClass();

Jak myślisz, w jakiej kolejności wykonają się konstruktory?

SimpleAbstract constructor
SimpleClass constructor

Pierwszy zostanie utworzony rodzic! Czyli klasa abstrakcyjna! Dopiero po utworzeniu rodzica, tworzy się jego dziecko!

05. Podsumowanie

W telegraficznym skrócie przedstawię wszystkie najważniejsze informacje dotyczące klas abstrakcyjnych.

  • Klasa abstrakcyjna posiada słowo kluczowe abstract.
  • Klasa abstrakcyjna może posiadać abstrakcyjne, oraz nie abstrakcyjne metody.
  • Możemy utworzyć bezpośrednio instancję z klasy abstrakcyjnej, jednak musimy podczas tworzenia zapewnić implementację wszystkich zadeklarowanych metod.
  • Klasa abstrakcyjna może posiadać metody finalne (metody które nie mogą zostać nadpisane)
  • Klasa abstrakcyjna może posiadać metody statyczne (metody które możemy użyć bez tworzenia konkretnej instancji)
  • Klasa abstrakcyjna może posiadać konstruktor, który zawsze wykona się przed konstruktorem klasy która rozszerza daną abstrakcję.
  • Klasa abstrakcyjna, tak samo jak normalna klasa może posiadać swoje pola.

Ostatnią, oraz jedną z najważniejszych rzeczy jakie należy powiedzieć o klasach abstrakcyjnych, jest to, iż klasa może rozszerzać tylko jedną klasę abstrakcyjną! W odróżnieniu od interfejsów, których jedna klasa może implementować wiele.

Zdaje się że to najważniejsze informacje które dotyczące klas abstrakcyjnych. Pozostaje jeszcze pytanie kiedy używać interfejsów, a kiedy klas abstrakcyjnych, ale tym tematem zajmiemy się w jednym z kolejnych części kursu programowania w Javie! 😊


Interfejsy - Część druga

W poprzednim wpisie, poznaliśmy czym są Interfejsy w Javie. Jednak temat interfejsów jest znacznie szerszy niż zostało to przedstawione w poprzednim wpisie 😊 Dziś kontynuujemy tę wędrówkę!

01. Metody domyślne w Interfejsach

Pierwszą poważną zmianą w interfejsach dostaliśmy wraz z nadejściem Javy 8. Mowa oczywiście o metodach domyślnych. W poprzednim wpisie napisałem że o interfejsie możemy myśleć jak o kolekcji abstrakcyjnych metod, to nie do końca jest prawdą od Javy 8 😉

Metoda domyślna w interfejsie to taka która posiada swoje ciało, i nie musi być implementowana przez klasa która implementuje dany interfejs. Od tej zasady jest wyjątek, kiedy klasa implementuje więcej niż jeden interfejs, w którym znajdują się takie same metody domyślne, wówczas klasa implementująca musi stworzyć własną implementację tej metody.

No to proste przykłady! Interfejs z metodą domyślną:

public interface SimpleInterface {

    void method();

    default void simpleDefault() {
        System.out.println("inside SimpleInterface simpleDefault method");
    }
    
}

Oraz prosta klasa która implementuje nasz interfejs:

public class SimpleClass implements SimpleInterface{

    @Override
    public void method() {
        System.out.println("this method is empty");
    }
    
}

Zauważ że klasa implementująca interfejs nie posiada własnej implementacji metody domyślnej interfejsu. Mimo to, bez żadnych przeszkód możemy z niej skorzystać! 😊

public class Main {

    public static void main(String[] args) {
        SimpleClass simpleClass = new SimpleClass();
        simpleClass.simpleDefault();
    }

}

Tak jak wspomniałem wcześniej sytuacja zmienia się odrobinę w momencie kiedy klasa implementuje więcej niż jeden interfejs, w którym znajduje się dokładnie taka sama metoda domyślna.

Tworzymy kolejny interfejs:

public interface SecondInterface {

    default void simpleDefault() {
        System.out.println("inside SecondInterface simpleDefault method");
    }
    
}

Oraz dokładamy ten interfejs do naszej klasy:

public class SimpleClass implements SimpleInterface, SecondInterface {

    @Override
    public void method() {
        System.out.println("this method is empty");
    }

    @Override
    public void simpleDefault() {
        System.out.println("konieczna implementacja!");
    }
}

Zauważ że w tym momencie metoda domyślna musiała zostać zaimplementowana.

Ważne jest aby wspomnieć o tym że interfejs może posiadać wiele metod domyślnych 😊 Java 8 to był naprawdę przełom pod wieloma względami! 😊

02. Metody statyczne w Interfejsach

Metody statyczne w interfejsie są nieco podobne do metod domyślnych, posiadają ciało metody! Jednak jest między nimi zasadnicza różnica, metody statycznej nie da się nadpisać (zaimplementować) w klasie która implementuje dany interfejs. Metoda statyczna interfejsu należy tylko i wyłącznie do konkretnego interfejsu, dzięki temu nie ma ryzyka że będziemy musieli wykonywać jakąś pracę w związku z tym że jakaś klasa implementuje różne interfejsy z taką samą statyczną metodą! Pozwala to na napisanie metody w interfejsie, która nie zostanie nadpisana.

public interface SimpleInterface {

    void method();

    default void simpleDefault() {
        System.out.println("inside SimpleInterface simpleDefault method");
    }

    static boolean simpleTest(String someString) {
        return someString != null && !someString.isEmpty();
    }

}

Klasa implementująca nic się nie zmieniła! 😊 Żeby skorzystać ze statycznej metody interfejsu musimy odnieść się bezpośrednio do niego!

SimpleInterface.simpleTest("abc");

Podsumowanie

Dzisiejszy wpis był kontynuacją poprzedniego wpisu o interfejsach, oraz kolejną częścią podstawowego kursu programowania w Javie! W tym momencie mamy już wystarczającą wiedzę żeby w pełni korzystać z interfejsów w Javie! Serdecznie zapraszam do eksperymentowania, to naprawdę potężne narzędzie w naszym arenale! Korzystajcie z nich często 😉


Interfejsy - Podstawa

W dzisiejszej lekcji poruszymy kolejny aspekt programowania obiektowego w Javie. Dziś przedstawię wam koncept Interfejsów w Javie, o co w tym wszystkim chodzi, jak możemy je wykorzystać, oraz dlaczego interfejsy są takie ważne! Zapraszam!

01. Czym jest Interfejs w Javie?

Interfejs w pewnym sensie jest podobny do klasy (o klasach możesz przeczytać w tym wpisie). Z pewnymi dość istotnymi różnicami 😉 Jak to zwykle bywa diabeł tkwi w szczegółach 😛

O Interfejsie możemy myśleć jak o kolekcji abstrakcyjnych metod, które muszą zostać zaimplementowane przez klasę która implementuje dany interfejs.

Powyższe zdanie mogło zabrzmieć trochę strasznie, lub może wydawać się skomplikowane, w rzeczywistości jednak jest to bardzo proste, najłatwiej będzie to zaprezentować na prostym przykładnie, stwórzmy interfejs który pozwoli nam obliczać pole powierzchni:

public interface Shape {
    double calculateArea();
}

Zauważ że występuje tutaj słowo kluczowe interface! Jest to podstawowe odróżnienie klasy od interfejsu. Jak łatwo zauważyć, metoda którą utworzyliśmy w naszym interfejsie, posiada jedynie deklarację, bez żadnego ciała. Jest to metoda abstrakcyjna, a jej „ciało” zostanie stworzone w klasie która będzie implementować nasz interfejs.

Teraz utworzymy klasę, która będzie implementować nasz interfejs:

public class Triangle implements Shape {
    
    @Override
    public double calculateArea() {
        return 0;
    }
}

Klasa która implementuje interfejs, musi posiadać implementację wszystkich metod zadeklarowanych w interfejsie, w naszym przypadku jest to jedna metoda calculateArea. Nasze IDE zawsze nam podpowie o tym że musimy napisać implementacje dla konkretnych metod. W tym momencie mamy pusty szkielet naszej metody.

Aby wyliczyć pole trójkąta musimy znać wysokość, oraz długość podstawy (tak tak, to tylko jedna z metody wyliczenia pola powierzchni w trójkącie 😉).  Tworzymy więc odpowiednia pola w naszej klasie, oraz konstruktor:

public class Triangle implements Shape {

    private double baseLength;
    private double heightLength;

    public Triangle(double baseLength, double heightLength) {
        this.baseLength = baseLength;
        this.heightLength = heightLength;
    }

    @Override
    public double calculateArea() {
        return 0;
    }
}

Nasza klasa posiada dwa pola, długość podstawy, oraz wysokość, klasa posiada również konstruktor który przyjmuje oba te parametry. Czas uzupełnić naszą metodę calculateArea. Cały kod naszej klasy w tym momencie wygląda tak:

public class Triangle implements Shape {

    private double baseLength;
    private double heightLength;

    public Triangle(double baseLength, double heightLength) {
        this.baseLength = baseLength;
        this.heightLength = heightLength;
    }

    @Override
    public double calculateArea() {
        System.out.println("Calculate area of Triangle");
        return 0.5*(baseLength*heightLength);
    }
}

Dodaliśmy wypisanie informacji na konsolę, oraz implementację metody która wyliczy nam pole powierzchni dla trójkąta. A teraz użyjemy naszej klasy, w metodzie main zrobimy tak:

Triangle triangle = new Triangle(5,3);
System.out.println(triangle.calculateArea());

Na konsoli zostanie wypisana informacja że jest liczona powierzchnia dla trójkąta, oraz zostanie wypisana wyliczona wartość pola powierzchni 😊

Okej, ale po co nam tak naprawdę ten interfejs? Przecież mogliśmy to wszystko zrobić bez żadnego interfejsu! Magia dopiero się zaczyna! 😊 Zróbmy kolejną klasę która będzie implementować nasz interfejs:

public class Square implements Shape {

    private double a;

    public Square(double a) {
        this.a = a;
    }

    @Override
    public double calculateArea() {
        System.out.println("Calculate area of Square");
        return a*a;
    }
}

A teraz użyjmy jej w naszej klasie mian:

public static void main(String[] args) {
    Triangle triangle = new Triangle(5,3);
    System.out.println(triangle.calculateArea());

    Square square = new Square(12);
    System.out.println(square.calculateArea());

}

Jeśli teraz pomyślałeś coś w stylu „Okej, mamy dwie klasy które implementują nasz interfejs, ale co z tą magią? Ciągle ten sam efekt mogliśmy osiągnąć gdyby te klasy po prostu miały taką metodę bez żadnych interfejsów!”

Miałbyś 100% racji! A teraz przejdziemy do sedna całej zabawy! Tworzymy metodę która jako parametr przyjmie nasz interfejs a nie jego konkretną implementację!

public static void calculateShapeArea(Shape shape) {
    System.out.println(shape.calculateArea());
}

A nasza metoda main może teraz wyglądać tak:

public static void main(String[] args) {
    Triangle triangle = new Triangle(5,3);
    calculateShapeArea(triangle);
    Square square = new Square(12);
    calculateShapeArea(square);
}

public static void calculateShapeArea(Shape shape) {
    System.out.println(shape.calculateArea());
}

Metoda tak jak wcześniej powiedzieliśmy, jako parametr przyjmuje interfejs (w naszym przypadku jest to interfejs Shape), a nie jakąś konkretną jego implementację (Triangle lub Square). Metoda nie wie tak naprawdę co konkretnego do niej przekazaliśmy, wie natomiast że co by to nie było, jest to coś co implementuje interfejs Shape, więc może wywołać metodę która jest w nim zadeklarowana!

Inny przykład, ostatnio pisałem o tablicach oraz pętlach. Użyjmy ich wraz z naszym interfejsem:

public static void main(String[] args) {
    Triangle triangle = new Triangle(5,3);
    Square square = new Square(12);

    Shape[] array = {triangle, square};
    for (Shape shape: array) {
        System.out.println(shape.calculateArea());
    }

}

Tak jak widzisz, stworzyliśmy tablicę która przechowuje Shape, nasza klasa Triangle jak i Square implementuje Shape, zatem obie te klasy mogą być przechowywane w tej samej tablicy, dzięki czemu możemy przeiterować się po wszystkich elementach danej tablicy i wywołać metodę która znajduje się w interfejsie 😊

02. Podsumowanie

Korzystając z interfejsów możemy stworzyć nieograniczoną liczbę różnych klas które będę posiadać własną implementację danego interfejsu. Interfejs mówi nam o tym iż coś posiada daną funkcjonalność, jak ta funkcjonalność działa wewnątrz zależy już od konkretnej implementacji.

W dzisiejszej lekcji opisałem podstawy oraz koncept związany z Interfejsem w Javie. Jak widzisz Interfejsy dają nam naprawdę ogromne możliwości. W programowaniu obiektowym istnieje cały koncept oparty na tym, że powinniśmy programować z wykorzystaniem interfejsów zamiast konkretnych jego implementacji! (“Program to an interface, not an implementation.”)

Dodatkowo interfejsy w Javie przeszły pewne usprawnienia od czasu pierwszych wersji Javy, jednak o tym co zostało dodane porozmawiamy w kolejnej lekcji! 😊