Dawid Nowak – Dev Foundry http://devfoundry.pl programowanie • java • spring • kursy Thu, 13 May 2021 15:17:56 +0000 pl-PL hourly 1 https://wordpress.org/?v=5.0.16 http://devfoundry.pl/wp-content/uploads/2018/05/cropped-DEVfoundry_SYMBOL_512x512-32x32.png Dawid Nowak – Dev Foundry http://devfoundry.pl 32 32 Heap, Stack i String Pool w Javie http://devfoundry.pl/heap-stack-i-string-pool-w-javie/ http://devfoundry.pl/heap-stack-i-string-pool-w-javie/#respond Fri, 30 Apr 2021 16:59:59 +0000 http://devfoundry.pl/?p=1249 System zarządzania pamięcią w Javie opiera się na dwóch konceptach: Stacku czyli Stosie oraz Heap, a więc Kopcu. Przyjrzyjmy się tym dwóm strukturom danych nieco bliżej. Stack i wątki W ramach działania aplikacji Javowej, począwszy od metody main, na Stacku (Stosie) pojawiają się jedna na drugiej ramki zawierające zmienne metod. Przestrzegają przy tym zasady LIFO (Last In, First Out). Gdy dana dana metoda zakończy swoje działanie, to jest automatycznie wypychana ze Stacka. Każdy wątek ma swój Thread Stack, na którym przechowywane ...

Czytaj dalej...Heap, Stack i String Pool w Javie

The post Heap, Stack i String Pool w Javie appeared first on Dev Foundry.

]]>
System zarządzania pamięcią w Javie opiera się na dwóch konceptach: Stacku czyli Stosie oraz Heap, a więc Kopcu. Przyjrzyjmy się tym dwóm strukturom danych nieco bliżej.

Stack i wątki

W ramach działania aplikacji Javowej, począwszy od metody main, na Stacku (Stosie) pojawiają się jedna na drugiej ramki zawierające zmienne metod. Przestrzegają przy tym zasady LIFO (Last In, First Out). Gdy dana dana metoda zakończy swoje działanie, to jest automatycznie wypychana ze Stacka.

Każdy wątek ma swój Thread Stack, na którym przechowywane są zmienne lokalne, ale jeśli są to zmienne referencyjne, to – uwaga – same obiekty do których odnoszą się te zmienne, znajdują się na Kopcu.

Załóżmy dla przykładu, że dana klasa implementująca interfejs Runnable ma jakieś pole value typu Integer. Wtedy jeśli z tej klasy zostaną utworzone np. 2 wątki, to każdy z tych wątków będzie korzystał z tego samego obiektu do którego odnosi się zmienna value. Dzieje się tak właśnie dlatego, że ten obiekt znajduje się na Heap, a nie lokalnych stackach tych wątków.

Ale jeśli dodatkowo ta sama klasa ma jakąś metodę, która będzie miała parametr int i, to kopię tego samego parametru i będą miały obydwa wątki, każdy na swoim lokalnym Stacku.

Kopiec

Jak już wspominałem wyżej – wszystkie obiekty w Javie znajdują się na Kopcu.

W cyklu działania danej aplikacji, obiekty mogą należeć do różnych tzw. Generations. Wyróżnia się kilka Generations obiektów i to determinuje ich miejsce w Kopcu.

Young Generation:
Eden Space – tu trafiają nowo utworzone obiekty.
Survivor Space – tu trafiają obiekty, które przetrwają cykl Garbage Collection w Eden Space.

Old Generation:
Tenured Space – tu trafiają obiekty, które przetrwały jakiś czas w Survivor Space.

Permanent Generation/Metaspace:
Miejsce w pamięci, które zawiera różne metadane a także static metody oraz zmienne.

UWAGA: Od Javy 8 to miejsce nazywa się Metaspace i różni się tym of Permanent Generation, że dynamicznie zmienia swój rozmiar w runtime aplikacji, czego Permanent Generation nie potrafiło.

Jak widać obiekty i ich miejsce w Kopcu jest też ściśle związane z Garbage Collectorem, ale o tym w innym wpisie 🙂

Różnica między Stosem i Kopcem

Pomijając oczywiste różnice i informacje, które opisałem powyżej, Stack i Heap różnią się od siebie jeszcze pod kilkoma innymi względami:

  • zmienne w Stacku mają długość życia przypisaną do trwania metody, a obiekty w Kopcu zależą od Garbage Collectora i ew. mogą żyć przez całą długość życia aplikacji,*
  • dostęp do zmiennych w Stacku jest o wiele szybszy niż do obiektów w Kopcu,
  • Heap nie jest Thread Safe i trzeba odpowiednio zarządzać dostępem do obiektów.

*To samo teoretycznie może dotyczyć jakiejś zmiennej znajdującej się na końcu metody main, ale… zakładam że to dość rzadki przypadek 🙂

String Pool

String Pool przed Javą 7 był częścią Permanent Generation. Od Javy 7 String Pool jest częścią Heap space.

Stringi w Javie są immutable, to znaczy, że jeśli napiszemy:

String s1 = "Test";
String s2 = "Test";

To Java nie przydzieli dodatkowego miejsca w String Pool dla stringa, na którego wskazuje zmienna “s2”. Tylko “s1” i “s2” będą wskazywały na to samo miejsce w pamięci.

UWAGA: Wyjątkiem jest sytuacja, kiedy stworzymy nowego Stringa za pomocą słowa kluczowego “new”, np.:

String s1 = "Test";
String s2 = new String("Test");

Wtedy nawet jeśli taki literał już istnieje w String Pool, to Java utworzy nowy obiekt typu String i umieści go na Heap.

Zaletą String Poola w Heap Space jest to, że Stringi które nie są z niczym powiązane (unreferenced Strings) podlegają usunięciu przez Garbage Collector. A w przypadku Permanent Generation przed Javą 7 nie były usuwane, bo.. były częścią Permanent Generation, której nie tykał się Garbage Collector. A jako że Permanent Generation było obszarem o stałym rozmiarze, to można było dość łatwo osiągnąć Out of Memory Error przez tworzenie zbyt dużej ilości nowych obiektów typu Stringów.

The post Heap, Stack i String Pool w Javie appeared first on Dev Foundry.

]]>
http://devfoundry.pl/heap-stack-i-string-pool-w-javie/feed/ 0
Słowo kluczowe Static w Javie http://devfoundry.pl/slowo-kluczowe-static-w-javie/ http://devfoundry.pl/slowo-kluczowe-static-w-javie/#respond Sun, 10 Jan 2021 11:48:21 +0000 http://devfoundry.pl/?p=1239 Pola oraz metody typu static należą do klasy, a nie do jej danej instancji, czyli obiektu. Dane z nich zapisywane są do specjalnego miejsca w pamięci, a dokładnie do Permanent Generation (od Javy 8 jest to tzw. Metaspace) poprzez Class Loadera, który… wczytuje daną klasę. Dlatego też pierwszy kod, który wykonuje się w Javie pochodzi z bloków static. Najpierw klasa wczytywana jest do Class Loadera, a dopiero później jakiekolwiek obiekty tej klasy mogą zostać utworzone. I dopiero wtedy wykonywany jest ...

Czytaj dalej...Słowo kluczowe Static w Javie

The post Słowo kluczowe Static w Javie appeared first on Dev Foundry.

]]>
Pola oraz metody typu static należą do klasy, a nie do jej danej instancji, czyli obiektu. Dane z nich zapisywane są do specjalnego miejsca w pamięci, a dokładnie do Permanent Generation (od Javy 8 jest to tzw. Metaspace) poprzez Class Loadera, który… wczytuje daną klasę.

Dlatego też pierwszy kod, który wykonuje się w Javie pochodzi z bloków static. Najpierw klasa wczytywana jest do Class Loadera, a dopiero później jakiekolwiek obiekty tej klasy mogą zostać utworzone. I dopiero wtedy wykonywany jest kod, który znajduje się w konstruktorze danej klasy.

Pola static

Słowo kluczowe static sprawia, że dane pole będzie przypisane do klasy, a nie do danej instancji tej klasy, a więc do obiektu tej klasy.

Może to być przydatne na przykład w sytuacji, gdy wiemy i chcemy zrobić tak, żeby wszystkie obiekty danej klasy miały jakąś część wspólną. Taką częścią może być właśnie jakaś wartość pola statycznego.

Zmienną statyczną wywołujemy pisząc przed nią nazwę klasy:

Bicycle.numberOfBicycles

Aczkolwiek można się do niej odnieść również poprzez zmienną referencyjną, chociaż jest to odradzane, bo wtedy nie widać na pierwszy rzut oka, że wywoływana metoda jest typu static:

myBike.numberOfBicycles

Wartość zmiennej statycznej można ustawić ręcznie przy deklaracji, w bloku static, ale również w konstruktorze. Wtedy przy każdorazowym tworzeniu instancji danej klasy, będzie ustawiana/inkrementowana wartość takiej zmiennej. Można w ten sposób na przykład zaimplementować licznik instancji danej klasy (w konstruktorze inkrementuje się wartość zmiennej statycznej).

Metody statyczne

Bardzo podobnie do zmiennych, statyczne metody należą do klasy, a nie do jej instancji.

Można się do nich odnosić przez nazwę klasy lub zmienną referencyjną (co ponownie – nie jest zalecane).

Można z nich normalnie korzystać z jedną uwagą.

UWAGA: W obrębie danej klasy, ze środka metod static NIE MOŻNA bezpośrednio wywoływać zwykłych zmiennych lub metod, bo należą one do danej instancji klasy, a nie do klasy jako takiej (więc kod w metodzie statycznej nie wiedziałby do jakiej instancji ma się zastosować). Dotyczy to również słowa kluczowego “this”, bo metoda statyczna nie zna żadnej instancji, do której może odnosić się “this”. Oczywiście ze środka metod static nadal można odnosić się do zwykłych zmiennych i metod, jeśli odbywa się to za pomocą zmiennej referencyjnej danego obiektu.

Poza tym dowolna kombinacja zmiennych i metod oraz ich wywołań jest dozwolona.

Stałe – Constants

Słowo kluczowe static w kombinacji z final, daje nam coś na kształt stałej, ponieważ jest jedna jej instancja oraz po przypisaniu jej jakiejś wartości, nie można jej zmienić. Zatem jeśli od razu przy deklaracji mamy też inicjalizację takiej zmiennej, to faktycznie jest ona stałą:

private static final double PI = 3.141592653589793;

Blok static

Kod z bloków static jest wykonywany w momencie wczytywania tej klasy przez Class Loadera (co dzieje się gdy JVM po raz pierwszy natknie się na tę klasę w kodzie), podobnie jak w przypadku metod oraz zmiennych typu static. Taką inicjalizację można też wymusić gdzieś w kodzie, który wiemy, że będzie się wykonywał jako pierwszy lub jeden z pierwszych, pisząc:

StaticClass.init()

lub:

Class.forName(StaticClass)

Dana klasa może mieć więcej niż jeden blok static.

Alternatywą dla bloku static jest wykorzystanie prywatnej, statycznej metody przy inicjalizacji pola static:

class ExampleClass {
    public static String stringVariable = initializeClassVariable();
        
    private static String initializeClassVariable() {

        //kod inicjalizacyjny
    }
}

The post Słowo kluczowe Static w Javie appeared first on Dev Foundry.

]]>
http://devfoundry.pl/slowo-kluczowe-static-w-javie/feed/ 0
Wzorzec projektowy Fasada http://devfoundry.pl/wzorzec-projektowy-fasada/ http://devfoundry.pl/wzorzec-projektowy-fasada/#respond Wed, 18 Nov 2020 10:28:42 +0000 http://devfoundry.pl/?p=1173 Fasada jest jednym ze wzorców strukturalnych. Na pierwszy rzut oka może wydawać się podobna do wzorca Adapter, jednak różni je przeznaczenie. Celem wzorca Adapter jest modyfikacja danego interfejsu tak, aby dostosować go do potrzeb klienta. Natomiast celem Fasady jest zapewnienie klientowi uproszczonego interfejsu dla danego systemu lub jego podsystemów. Najczęstszym zadaniem Fasady jest zatem izolacja klienta od podsystemu, czyli wewnętrznych metod oraz logiki biznesowej. Jeśli więc klient ma nie mieć bezpośredniego dostępu do systemu lub podsystemów ze względu bezpieczeństwa, to ...

Czytaj dalej...Wzorzec projektowy Fasada

The post Wzorzec projektowy Fasada appeared first on Dev Foundry.

]]>
Fasada jest jednym ze wzorców strukturalnych. Na pierwszy rzut oka może wydawać się podobna do wzorca Adapter, jednak różni je przeznaczenie. Celem wzorca Adapter jest modyfikacja danego interfejsu tak, aby dostosować go do potrzeb klienta. Natomiast celem Fasady jest zapewnienie klientowi uproszczonego interfejsu dla danego systemu lub jego podsystemów.

Najczęstszym zadaniem Fasady jest zatem izolacja klienta od podsystemu, czyli wewnętrznych metod oraz logiki biznesowej.

Jeśli więc klient ma nie mieć bezpośredniego dostępu do systemu lub podsystemów ze względu bezpieczeństwa, to wzorzec Fasada nadaje się do tego idealnie.

Po więcej szczegółów, zapraszamy do filmu poniżej. Dowiesz się w nim między innymi:

  • jakie są zalety i wady Fasady,
  • poznasz schemat działania Fasady,
  • zobaczysz przykład negatywny,
  • poznasz poprawną implementację wzorca Fasada.

 

Można powiedzieć, że Fasada jest jak interfejs użytkownika: dostępne i widoczne są wybrane funkcjonalności. Użytkownik nie musi znać ani widzieć dokładnie działania systemu wewnętrznego, ponieważ potrzebne funkcjonalności są mu udostępniane na zewnątrz.

Uwaga: jeśli naszym celem nie jest zapewnienie bezpieczeństwa, a jedynie zapewnienie uproszczonego interfejsu dla klienta, to nic nie stoi na przeszkodzie, aby klient nadal miał dostęp do danych klas i metod z podsystemu.

Jest to dość prosty wzorzec i na pewno warto z niego skorzystać, zwłaszcza tam, gdzie potrzebujemy schować pewną część naszego kodu przed klientem lub też jeśli chcemy dostarczyć klientowi uproszczoną wersję danej funkcjonalności.

Jakie są zalety Fasady?

Fasada rozdziela klienta od podsystemów danego systemu. Więc może zapewniać bezpieczeństwo, bo klient nie ma bezpośredniego dostępu do metod podsystemu.

Klient jest oddzielony od niepotrzebnej dla niego wiedzy złożoności działania danego podsystemu.

Natomiast co do wad:

Trzeba pamiętać, że utworzona przez nas fasada jest zależna od klas podsystemów. Jeśli ich działanie ulegnie zmianie, to musimy również aktualizować fasadę. W przeciwnym razie jej działanie będzie nieprawidłowe.

The post Wzorzec projektowy Fasada appeared first on Dev Foundry.

]]>
http://devfoundry.pl/wzorzec-projektowy-fasada/feed/ 0
Testy jednostkowe – Mocki http://devfoundry.pl/testy-jednostkowe-mocki/ http://devfoundry.pl/testy-jednostkowe-mocki/#comments Wed, 10 Jul 2019 07:10:53 +0000 http://devfoundry.pl/?p=1121 Mocki to obiekty, które imitują zachowanie prawdziwych obiektów i prawdziwego kodu. Zadaniem programisty jest zaprogramowanie odpowiedniego działania mocka. Ten wpis jest drugą częścią miniserii o stubach oraz mockach. Poznamy w nim zalety mocków, a także ich ogólną charakterystykę i zastosowanie. Pod tym adresem znajdziesz część pierwszą, w której omawiane są stuby. Jak mocki, to Mockito Aby w ogóle móc skorzystać z obiektów mockowych, należy dodać do projektu zależność w postaci frameworka Mockito. Najlepiej ściągnąć najnowszą wersję, aktualnie jest to wersja 2.25. ...

Czytaj dalej...Testy jednostkowe – Mocki

The post Testy jednostkowe – Mocki appeared first on Dev Foundry.

]]>
Mocki to obiekty, które imitują zachowanie prawdziwych obiektów i prawdziwego kodu. Zadaniem programisty jest zaprogramowanie odpowiedniego działania mocka.

Ten wpis jest drugą częścią miniserii o stubach oraz mockach. Poznamy w nim zalety mocków, a także ich ogólną charakterystykę i zastosowanie. Pod tym adresem znajdziesz część pierwszą, w której omawiane są stuby.

Jak mocki, to Mockito

Aby w ogóle móc skorzystać z obiektów mockowych, należy dodać do projektu zależność w postaci frameworka Mockito. Najlepiej ściągnąć najnowszą wersję, aktualnie jest to wersja 2.25.

My polecamy skorzystanie z zależności, która jest przystosowana do współpracy z frameworkiem JUnitmockito-junit-jupiter.

Odpowiedni artefakt znajdziemy korzystając na przykład z wyszukiwarki na stronie maven.org lub też bezpośrednio z tego odnośnika. Pod tym adresem znajdziemy gotowy plik *.jar lub też gotowy wpis, który możemy wkleić do pliku konfiguracyjnego Mavena lub Gradla.

Czym są mocki?

Mocki to obiekty, których zadaniem jest symulacja działania normalnych obiektów i kodu. Natomiast zadaniem programisty jest odpowiednie zaprogramowanie działania mocka.

Na przykład: jeżeli metoda zostanie wywołana obiekcie mockowym X, to ma zwrócić wartość Y.

Mocki mogą być tworzone dynamicznie w czasie runtime’u aplikacji i są znacznie bardziej elastyczne w porównaniu do stubów. Zapewniają też znacznie więcej funkcjonalności.


Jeśli chcesz prześledzić zmiany krok po kroku w formie wideo, to zapraszamy na nasz kanał na YouTube.


Jak je tworzyć?

Aby porównać mocki bezpośrednio ze stubami, utworzymy klasę testową AccountServiceTest. Jej zadanie będzie takie samo, jak klasy testowej AccountServiceStubTest, gdzie testowaliśmy metodę z serwisu AccountService. (we wpisie z zeszłego tygodnia dotyczącym stubów)

Możemy więc skopiować cały kod z klasy AccountServiceStubTest. Nasza klasa testowa będzie zatem wyglądała tak:

@Test
void getAllActiveAccounts() {

    //given
    AccountRepository accountRepositoryStub = new AccountRepositoryStub();
    AccountService accountService = new AccountService(accountRepositoryStub);

    //when
    List accountList = accountService.getAllActiveAccounts();

    //then
    assertThat(accountList, hasSize(2));

}

Aby teraz zamienic stuba na obiekt mockowy, możemy skorzystać ze specjalnej metody frameworka Mockitomock. Ta metoda pozwala nam na utworzenie obiektu mockowego z danej klasy. W naszym przykładzie będzie to oczywiście klasa AccountRepository:

AccountRepository accountRepositoryMock = mock(AccountRepository.class);

I takiego mocka możemy teraz przekazać do konstruktora klasy AccountService:

AccountRepository accountRepositoryMock = mock(AccountRepository.class);
AccountService accountService = new AccountService(accountRepositoryMock);

Zatem jak było widać na powyższym przykładzie, w bardzo prosty sposób jesteśmy w stanie zamienić obiekt stubowy na obiekt mockowy.

Jednak jeśli teraz uruchomimy tę metodę testową, to asercja nie zostanie spełniona, a test nie przejdzie. W wyniku zobaczymy, że była oczekiwana wartość 2, a w rzeczywistości zwrócona została wartość 0, a więc pusta lista. Dlaczego?

Miłe mocki

Dzieje się tak dlatego, że w Mockito wykorzystywane są tak zwane „nice mocki„. Działają one w specyficzny sposób. Załóżmy, że mamy taką sytuację: w klasie, którą mockujemy znajdują się metody, które zwracają jakieś wartości. Wtedy domyślnie Mockito przy wywołaniu takich metod postara się zwrócić jakieś sensowne wartości, a nie na przykład po prostu  wartość null.

W naszym przykładzie, gdzie zwracana jest lista obiektów typu Account, Mockito zwraca pustą listę. A jeśli metoda miałaby zwracać wartości Integer, to Mockito zwróciłoby wartość 0. Jeśli metoda zwracałaby wartość boolean, to Mockito zwróciłoby wartość false. Więcej informacji można znaleźć w dokumentacji Mockito

W taki sposób będą zachowywały się mocki, jeśli nie zaprogramujemy dla nich żadnego działania. Ale właśnie… przecież możemy to zachowanie zaprogramować!

Szybko, łatwo i przyjemnie

Najpierw musimy przygotować listę danych testowych. Na szczęście mamy już taki zestaw w naszej klasie stubowej, znanej z poprzedniego wpisu. A żeby nie zaśmiecać naszej metody testowej, to dodatkowo ten kod umieścimy w metodzie pomocniczej:

private List prepareAccountData() {
    Address address1 = new Address("Kwiatowa", "33/5");
    Account account1 = new Account(address1);

    Account account2 = new Account();

    Address address2 = new Address("Piekarska", "12b");
    Account account3 = new Account(address2);

    return Arrays.asList(account1, account2, account3);
}

Dla przypomnienia, klasa Account ma postać:

class Account {

    private boolean active;
    private Address defaultDeliveryAddress;

    Account() {
        this.active = false;
    }

    Account(Address defaultDeliveryAddress) {
        this.defaultDeliveryAddress = defaultDeliveryAddress;
        if(defaultDeliveryAddress != null) {
            activate();
        } else {
            this.active = false;
        }
    }

    void activate() {
        this.active = true;
    }

    boolean isActive() {
        return this.active;
    }

    Address getDefaultDeliveryAddress() {
        return defaultDeliveryAddress;
    }

    void setDefaultDeliveryAddress(Address defaultDeliveryAddress) {
        this.defaultDeliveryAddress = defaultDeliveryAddress;
    }

}

Jeśli przy tworzeniu obiektu typu Account skorzystamy z konstruktora bezargumentowego lub też w konstruktorze oczekującym obiektu typu Address przekażemy wartość null, to wtedy utworzone konto będzie nieaktywne, a więc metoda isActive będzie zwracała wartość false. W przeciwnym razie oczywiście zwrócona będzie wartość true.

W naszym przykładzie utworzone zostały 3 konta: account1, account2 i account3. account2 zostało utworzone korzystając z konstruktora bezargumentowego, więc to konto będzie nieaktywne. account1 i accoun3 natomiast będą aktywne.

Możemy teraz wrócić do naszej metody testowej: getAllActiveAccounts . Tutaj chcielibyśmy móc zaprogramować takie działanie:

Jeżeli na mocku zostanie wywołana metoda getAllAccounts , to w wyniku powinna być zwrócona lista kont z metody prepareAccountData.

Aby to zrobić, korzystamy ze specjalnych metod pomocniczych z biblioteki Mockito: when oraz thenReturn:

when(accountRepository.getAllAccounts()).thenReturn(accounts);

Taka składnia jest popularna, ale może być dla kogoś myląca, bo mamy tu kombinację słów when i then, dość podobne do samych sekcji //given, //when i //then.

Zatem żeby zachować ducha Behaviour Driven Development (BDD), można skorzystać z innej kombinacji metod: given i willReturn.

Po zmianach cała metoda testowa prezentuje się następująco:

@Test
void getAllActiveAccounts() {

    //given
    List accounts = prepareAccountData();
    AccountRepository accountRepository = mock(AccountRepository.class);
    AccountService accountService = new AccountService(accountRepository);
    given(accountRepository.getAllAccounts()).willReturn(accounts);

    //when
    List accountList = accountService.getAllActiveAccounts();

    //then
    assertThat(accountList, hasSize(2));

}

I teraz gdy uruchomimy testy, czeka nas miły widok:

stuby mocki testy jednostkowe junit mockito dev foundry blog programowanie java spring kursy

Elastyczność

Powyższy przykład już jasno pokazuje, że mocki są znacznym ułatwieniem względem stubów. Chociażby dlatego, że nie musimy tworzyć osobnej klasy stubowej. Ale jeszcze większą zaletą jest to, jak łatwo możemy tworzyć kolejne scenariusze testowe.

Załóżmy, że chcielibyśmy przetestować sytuację, w której z bazy danych nie zwrócone zostaną żadne konta.

Zatem do klasy testowej dopisujemy kolejną metodę. Nazwiemy ją getNoActiveAccounts.

Część główną metody możemy dla ułatwienia skopiować z metody getAllActiveAccounts. Dodatkowo musimy tutaj dokonać zmiany logicznej. Oczekujemy, że lista kont, którą otrzymamy z bazy danych, będzie pusta. Zmieniamy zatem asercję z:

assertThat(accountList, hasSize(2));

na:

assertThat(accountList, hasSize(0));

I modyfikujemy zawartość metody thenReturn tak, aby obiekt mockowy faktycznie zwracał nam pustą listę (można tu skorzystać na przykład z utilsowej metody Javowej):

given(accountRepository.getAllAccounts()).willReturn(Collections.emptyList());

Ostatecznie cała metoda testowa wygląda tak:

@Test
void getNoActiveAccounts() {

    //given
    AccountRepository accountRepository = mock(AccountRepository.class);
    AccountService accountService = new AccountService(accountRepository);
    given(accountRepository.getAllAccounts()).willReturn(Collections.emptyList());

    //when
    List accountList = accountService.getAllActiveAccounts();

    //then
    assertThat(accountList, hasSize(0));

}

Możemy teraz ją uruchomić. Asercja powinna być spełniona i ponownie powinniśmy ujrzeć zielony kolor. To takie proste!

A jeśli w interfejsie pojawią się nowe metody, to również w niczym to nie przeszkodzi. Wtedy po prostu utworzy się stosownego mocka i zaprogramuje odpowiednie działanie.

Podsumowanie

Powyższe przykłady jasno pokazują dlaczego mocki są takie popularne i dlaczego zazwyczaj są lepszym rozwiązaniem od stubów. Dają programistom bardzo duże możliwości i zapewniają elastyczność w działaniu. Dzięki nim można też przetestować znacznie więcej scenariuszy testowych, oszczędzając przy tym dużo czasu.

The post Testy jednostkowe – Mocki appeared first on Dev Foundry.

]]>
http://devfoundry.pl/testy-jednostkowe-mocki/feed/ 1
Testy jednostkowe – Stuby http://devfoundry.pl/testy-jednostkowe-stuby/ http://devfoundry.pl/testy-jednostkowe-stuby/#comments Mon, 03 Jun 2019 07:24:38 +0000 http://devfoundry.pl/?p=1087 Stuby są wykorzystywane w sytuacji, gdy w testowanej klasie występują pewne zależności. Działanie tych zależności należy obsłużyć, ale problem pojawia się, jeśli nie mamy do nich lub do ich metod bezpośredniego dostępu. Właśnie w tych scenariuszach mogą nam pomóc stuby lub mocki. Ten wpis jest pierwszą częścią miniserii o stubach oraz mockach. Poznamy w nim wady oraz zalety stubów, a także ich ogólną charakterystykę i zastosowanie. W kolejnej części – bliżej przyglądamy się mockom. Scenariusz testowy Naszą bazą kodową, którą ...

Czytaj dalej...Testy jednostkowe – Stuby

The post Testy jednostkowe – Stuby appeared first on Dev Foundry.

]]>
Stuby są wykorzystywane w sytuacji, gdy w testowanej klasie występują pewne zależności. Działanie tych zależności należy obsłużyć, ale problem pojawia się, jeśli nie mamy do nich lub do ich metod bezpośredniego dostępu. Właśnie w tych scenariuszach mogą nam pomóc stuby lub mocki.

Ten wpis jest pierwszą częścią miniserii o stubach oraz mockach. Poznamy w nim wady oraz zalety stubów, a także ich ogólną charakterystykę i zastosowanie. W kolejnej części – bliżej przyglądamy się mockom.

Scenariusz testowy

Naszą bazą kodową, którą chcielibyśmy przetestować, jest aplikacja do zamawiania jedzenia online. Składa się z prostych klas takich jak Account, Order, Cart czy też Meal. W tym wpisie operować będziemy głównie na klasie Account:

class Account {

    private boolean active;
    private Address defaultDeliveryAddress;

    Account() {
        this.active = false;
    }

    Account(Address defaultDeliveryAddress) {
        this.defaultDeliveryAddress = defaultDeliveryAddress;
        if(defaultDeliveryAddress != null) {
            activate();
        } else {
            this.active = false;
        }
    }

    void activate() {
        this.active = true;
    }

    boolean isActive() {
        return this.active;
    }

    Address getDefaultDeliveryAddress() {
        return defaultDeliveryAddress;
    }

    void setDefaultDeliveryAddress(Address defaultDeliveryAddress) {
        this.defaultDeliveryAddress = defaultDeliveryAddress;
    }

}

Jest to prosta klasa typu POJO. Jedyną porcją logiki są konstruktory. Jeśli przy tworzeniu obiektu typu Account skorzystamy z konstruktora bezargumentowego lub też w konstruktorze oczekującym obiektu typu Address przekażemy wartość null, to wtedy utworzone konto będzie nieaktywne, a więc metoda isActive będzie zwracała wartość false. W przeciwnym razie oczywiście zwrócona będzie wartość true.

Nowa funkcjonalność

Teraz przyszedł czas, aby nieco rozbudować naszą aplikację. Chcemy dodać do niej nową funkcjonalność – możliwość pobrania listy klientów z bazy danych, tak aby wysłać im na przykład jakieś wiadomości promocyjne.

Aby to zrobić najpierw tworzymy nowy interfejs, który nazwiemy AccountRepository:

public interface AccountRepository {

    List getAllAccounts();

}

W interfejsie umieściliśmy deklarację jednej metody – getAllAccounts, która ma oczywiście służyć do pobrania kont wszystkich klientów.

Czas na implementację

Kolejnym naturalnym krokiem byłoby utworzenie implementacji powyższego interfejsu. Jednak aby to zrobić rzetelnie, należałoby najpierw przygotować całą konfigurację połączenia z bazą danych, co z kolei zajęłoby sporo czasu, a przecież nie to jest głównym tematem tego wpisu.


Jeśli chcesz prześledzić zmiany krok po kroku w formie wideo, to zapraszamy na nasz kanał na YouTube.


Można też na to spojrzeć w taki sposób, że w naszej aplikacji mamy dostęp tylko i wyłącznie do powyższej metody z interfejsu, a nie do samej jej implementacji. Może być przecież tak, że implementacją zajmuje się inny zespół albo też dane które nas interesują, możemy otrzymywać z jakiegoś innego, zewnętrznego systemu lub API. Załóżmy więc, że nie mamy bezpośredniego dostępu do metody implementującej. Zobaczmy więc, czy mimo takiej niedogodności będziemy w stanie odpowiednio przetestować interesujący nas kod.

Do wyciągania informacji o kontach potrzebny nam będzie serwis. Utworzymy więc stosowną klasę serwisową: AccountService, w którym będziemy mieli jedną zależność. Tą zależnością będzie oczywiście AccountRepository, więc dodajemy ją w konstruktorze:

class AccountService {

    private AccountRepository accountRepository;

    AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }


}

W powyższej klasie chcielibyśmy umieścić metodę, która będzie zwracała tylko konta aktywne. Nazwiemy ją getActiveAccounts:

List getAllActiveAccounts() {
    return accountRepository.getAllAccounts().stream()
            .filter(Account::isActive)
            .collect(Collectors.toList());
}

Jak widać metoda ta opiera się na danych, które ma zwracać metoda getAllAccounts, będąca implementacją metody z interfejsu AccountRepository. Metoda pobiera konta użytkowników, następnie filtruje je korzystając z metody isActive z klasy Account.

Klasa testowa

Nasz kod jest już gotowy do przetestowania. Tworzymy więc klasę testową AccountServiceStubTest, a w niej metodę getActiveAccounts:

@Test
void getAllActiveAccounts() {

    //given
    AccountService accountService = new AccountService();

W sekcji given chcieliśmy utworzyć obiekt klasy AccountService, ale pojawił się problem, ponieważ w konstruktorze musimy podać klasę, która implementuje interfejs AccountRepository, a my nie mamy instancji takiej klasy.

I właśnie w takiej sytuacji mogą nam pomóc mocki lub stuby. Zajmijmy się zatem stubami.

Czym są stuby?

Stuby to przykładowe implementacje jakiegoś kodu, którego zachowanie chcemy przetestować. Jeśli – tak jak w naszym przykładzie – nie mamy dostępu do prawdziwej metody, która będzie nam zwracała dane, to sami powinniśmy sobie taką metodę utworzyć. Musimy napisać ją w taki sposób, aby zwracała nam zestaw przykładowych danych.

Utwórzmy sobie zatem takiego stuba. Zaczniemy od stworzenia nowej klasy, a jako że stuby są nierozłącznie związane z testami jednostkowymi, to klasy stubowe najlepiej tworzyć w tej samej paczce, w której są normalne klasy testowe. Naszą klasę stubową nazwiemy dość nieoryginalnie – AccountRepositoryStub. Klasa ta powinna implementować interfejs AccountRepository i nadpisywać metodę getAllAccounts:

public class AccountRepositoryStub implements AccountRepository {

    @Override
    public List getAllAccounts() {
        Address address1 = new Address("Kwiatowa", "33/5");
        Account account1 = new Account(address1);

        Account account2 = new Account();

        Address address2 = new Address("Piekarska", "12b");
        Account account3 = new Account(address2);

        return Arrays.asList(account1, account2, account3);
    }

}

Implementacja tej metody jest bardzo prosta: tworzymy nowe konta użytkowników i zwracamy je w postaci listy – zgodnie z deklaracją metody getAllAccounts.

Teraz możemy wrócić do naszego testu i w konstruktorze serwisu przekazać naszego stuba:

@Test
void getAllActiveAccounts() {

    //given
    AccountRepository accountRepositoryStub = new AccountRepositoryStub();
    AccountService accountService = new AccountService(accountRepositoryStub);

A skoro wiemy jakie dane zwraca nasze stub, to wiemy też jaką asercję należy napisać, żeby test przeszedł pozytywnie. Zatem cały test prezentuje się tak:

@Test
void getAllActiveAccounts() {

    //given
    AccountRepository accountRepositoryStub = new AccountRepositoryStub();
    AccountService accountService = new AccountService(accountRepositoryStub);

    //when
    List accountList = accountService.getAllActiveAccounts();

    //then
    assertThat(accountList, hasSize(2));

}

Zakładaliśmy, że zwrócona lista powinna mieć 2 elementy, ponieważ dwa utworzone przez nas w metodzie stubowej konta miały w konstruktorze przekazany adres, a więc tylko te dwa konta są aktywne.

W ten właśnie sposób działają stuby.

Problemy stubowe

Jednak tutaj mamy tylko jeden scenariusz testowy, bo stub zwraca nam jeden, konkretny zestaw danych. A  co jeśli teraz chcielibyśmy przeprowadzić inny test? Co jeśli na przykład chcielibyśmy sprawdzić co będzie, jeśli z repozytorium nie zwrócone zostaną żadne konta aktywne?

W tej chwili nasz stub zwraca 2 konta aktywne i nie można w nim dodać kolejnej metody, ponieważ tylko i wyłącznie ta jedna, jedyna metoda może być implementacją metody z interfejsu. Zatem żeby przetestować inny scenariusz, trzeba by dodać kolejną klasę stubową implementującą interfejs AccountRepository. Następnie w tejże klasie utworzyć utworzyć kolejną metodę która zwracałaby inny zestaw danych.

I jak łatwo się domyślić wkrótce tych klas i metod stubowych mogłoby się zrobić bardzo dużo. A im bardziej skomplikowana metoda, tym więcej klas, i metod.

Jest jeszcze jedna kwestia. A co jeśli do interfejsu dodana zostanie kolejna metoda? Wtedy trzeba by ją implementować we wszystkich klasach stubowych, żeby móc je odpowiednio przetestować i uniknąć błędu kompilacji. Bardzo szybko może się zatem okazać, że klasy stubowe są bardzo ciężkie w utrzymaniu.

Podsumowanie

Dla bardzo prostych klas, metod i przykładów, w których jesteśmy pewni, że nie będą się rozrastać, stuby spełniają swoje zadanie. Jednak przy większej liczbie warunków testowych oraz przy możliwym rozroście interfejsów, są one niestety kiepskim rozwiązaniem. W tych sytuacjach znacznie lepiej skorzystać z mocków, którymi zajmiemy się w kolejnej części tej miniserii.

The post Testy jednostkowe – Stuby appeared first on Dev Foundry.

]]>
http://devfoundry.pl/testy-jednostkowe-stuby/feed/ 2
JUnit 5 – Extension Model http://devfoundry.pl/junit-5-extension-model/ http://devfoundry.pl/junit-5-extension-model/#comments Tue, 09 Apr 2019 07:04:40 +0000 http://devfoundry.pl/?p=1043 JUnit jest najpopularniejszym frameworkiem (lub – jak kto woli – biblioteką) stosowaną przy tworzeniu testów jednostkowych w Javie. W jego nowej wersji – JUnicie 5, miejsce Rules oraz test runnerów zajął nowy koncept – Extension Model. Daje on bardzo duże możliwości oraz elastyczność, ale dzieje się to kosztem gotowej funkcjonalności, którą zapewniały Rules z JUnita 4. Jak to drzewiej bywało? W JUnicie 4 mieliśmy do dyspozycji test runnery oraz Rules. Test runnery odpowiadały za uruchamianie testów i jeśli nie określiliśmy tego ...

Czytaj dalej...JUnit 5 – Extension Model

The post JUnit 5 – Extension Model appeared first on Dev Foundry.

]]>
JUnit jest najpopularniejszym frameworkiem (lub – jak kto woli – biblioteką) stosowaną przy tworzeniu testów jednostkowych w Javie. W jego nowej wersji – JUnicie 5, miejsce Rules oraz test runnerów zajął nowy koncept – Extension Model. Daje on bardzo duże możliwości oraz elastyczność, ale dzieje się to kosztem gotowej funkcjonalności, którą zapewniały Rules z JUnita 4.

Jak to drzewiej bywało?

W JUnicie 4 mieliśmy do dyspozycji test runnery oraz Rules.

Test runnery odpowiadały za uruchamianie testów i jeśli nie określiliśmy tego inaczej, to domyślnie wykorzystywany był test runner JUnitowy. Jeśli jednak chcieliśmy skorzystać z jakiegoś innego – gotowego lub customowego – test runnera, na przykład bardzo popularny był (i nadal zresztą jest) test runner do frameworka Mockito, to musieliśmy użyć adnotacji @RunWith:

@RunWith(MockitoJUnitRunner.class)

Natomiast Rules były wykorzystywane do tego, aby w klasach i metodach testowych korzystać z jakichś gotowych funkcjonalności.

Załóżmy, że we wszystkich metodach testowych, w wielu klasach testowych chcielibyśmy przed uruchomieniem testów przygotować specjalny plik tekstowy. W obrębie jednej klasy testowej, żeby nie powtarzać takiego kodu w każdej metodzie testowej, można by taki kod umieścić w metodzie z adnotacją @Before (w JUnicie 5 jest to adnotacja @BeforeEach). Dzięki temu odpowiednie działanie byłoby wykonane przed uruchomieniem każdej metody testowej.

Natomiast w tym przykładzie chcemy z takiego kodu korzystać w wielu klasach. Bez sensu byłoby zatem ten sam kod umieszczać w każdej klasie, w metodzie z adnotacją @Before. Wówczas kod nie powtarzałby się co prawda w obrębie klasy, ale w obrębie na przykład paczki – już tak. Żeby temu zaradzić możemy skorzystać z funkcjonalności, którą zapewnia nam zestaw Rules.

Żeby z nich skorzystać, należy najpierw utworzyć instancję interesującej nas klasy (już istniejącej, gotowej z paczki org.junit.rules) i umieścić nad nią adnotację @Rule:

@Rule
private TemporaryFolder temporaryFolder = new TemporaryFolder();

Tutaj mamy przykład klasy TemporaryFolder, dzięki której można utworzyć jakiś plik albo katalog, który jest automatycznie usuwany po zakończeniu danego testu jednostkowego:

temporary folder dev foundry blog programowanie java spring kursy
Przykład kilku metod z klasy TemporaryFolder

I tutaj, jak widać, korzystamy z gotowych, zdefiniowanych metod i możemy to robić w dowolnej ilości klas testowych bez ryzyka powtarzania kodu.


Jeśli chcesz prześledzić zmiany krok po kroku w formie wideo, to zapraszamy na nasz kanał na YouTube.


Wracamy do teraz – Junit 5

W JUnicie 5 mamy za to do dyspozycji Extension Model. Działa on na podobnej zasadzie do Rules, ale zamiast korzystać z gotowej funkcjonalności, mamy do dyspozycji zestaw interfejsów, których metody implementujemy, aby zaczepić się w odpowiednim miejscu life cycle (cyklu życia) danego testu, żeby wykonać tam jakieś działanie.

Cykl życia testu oznacza po prostu kolejność operacji, które są kolejno uruchamiane przez mechanizm przetwarzający daną metodę testową (w przypadku JUNita 5 jest to silnik JUnit Jupiter) w ramach danego testu.

I ta kolejność na przykładzie interfejsów z paczki org.junit.jupiter.api.extension, których metody możemy implementować, prezentuje się mniej więcej tak:

  • TestInstancePostProcessor
  • TestTemplateInvocationContext
  • ExecutionCondition
  • BeforeAllCallback
  • BeforeEachCallback
  • ParameterResolver
  • BeforeTestExecutionCallback
  • AfterTestExecutionCallback
  • TestExecutionExceptionHandler
  • AfterEachCallback
  • AfterAllCallback

Lub jeśli ktoś woli uproszczoną wersję graficzną:

junit 5 extension model dev foundry blog programowanie java spring kursy
JUnit 5 Extension Model – Lifecycle Callbacks

Pomarańczowym kolorem zaznaczone są znane nam adnotacje umieszczane nad metodami, które powinny być uruchamiane przed wszystkimi testami z danej klasy (adnotacja @BeforeAll), przed każdym pojedynczym testem w klasie (@BeforeEach), po każdym pojedynczym teście (@AfterEach) i po wszystkich testach w klasie (@AfterAll).

Natomiast niebieskim kolorem zaznaczone są Callbacks, będące interfejsami z cyklu życia danego testu. I to właśnie ich metody możemy implementować – wówczas będą one uruchamiane w takiej kolejności, jak na grafice powyżej.

Zatem, jeśli na przykład zaimplementujemy metodę interfejsu BeforeEachCallback, to kod takiej metody będzie wykonywany po metodzie oznaczonej adnotacją @BeforeAll, a przed metodą oznaczoną adnotacją @BeforeEach.

Praktyka czyni mistrza

Zobaczmy teraz w jaki sposób możemy to wykorzystać w jakimś przykładzie. Mamy już przygotowaną klasę testową OrderTest:

class OrderTest {

    private Order order;

    @BeforeEach
    void initializeOrder() {
        System.out.println("Before each");
        order = new Order();
    }

    @AfterEach
    void cleanUp() {
        System.out.println("After each");
        order.cancel();
    }

    @Test
    void testAssertArrayEquals() {

        //given
        int[] ints1 = {1, 2, 3};
        int[] ints2 = {1, 2, 3};

        //then
        assertArrayEquals(ints1, ints2);

    }

    @Test
    void mealListShouldBeEmptyAfterCreationOfOrder() {
        //then
        assertThat(order.getMeals(), empty());
        assertThat(order.getMeals().size(), equalTo(0));
        assertThat(order.getMeals(), hasSize(0));
        assertThat(order.getMeals(), emptyCollectionOf(Meal.class));

    }

    @Test
    void addingMealToOrderShouldIncreaseOrderSize() {

        //given
        Meal meal = new Meal(15, "Burger");
        Meal meal2 = new Meal(5, "Sandwich");

        //when
        order.addMealToOrder(meal);

        //then
        assertThat(order.getMeals(), hasSize(1));
        assertThat(order.getMeals(), contains(meal));
        assertThat(order.getMeals(), hasItem(meal));
        assertThat(order.getMeals().get(0).getPrice(), equalTo(15));

    }

    @Test
    void removingMealFromOrderShouldDecreaseOrderSize() {

        //given
        Meal meal = new Meal(15, "Burger");

        //when
        order.addMealToOrder(meal);
        order.removeMealFromOrder(meal);

        //then
        assertThat(order.getMeals(), hasSize(0));
        assertThat(order.getMeals(), not(contains(meal)));

    }

    @Test
    void mealsShouldBeInCorrectOrderAfterAddingThemToOrder() {

        //given
        Meal meal1 = new Meal(15, "Burger");
        Meal meal2 = new Meal(5, "Sandwich");

        //when
        order.addMealToOrder(meal1);
        order.addMealToOrder(meal2);

        //then
        assertThat(order.getMeals(), containsInAnyOrder(meal2, meal1));

    }

    @Test
    void testIfTwoMealListsAreTheSame() {

        //given
        Meal meal1 = new Meal(15, "Burger");
        Meal meal2 = new Meal(5, "Sandwich");
        Meal meal3 = new Meal(11, "Kebab");

        List meals1 = Arrays.asList(meal1, meal2);
        List meals2 = Arrays.asList(meal1, meal2);

        //then
        assertThat(meals1, is(meals2));

    }

}

Znajduje się w niej sześć metod testowych i dwie metody z adnotacjami @BeforeEach oraz @AfterEach.

Są to dość małe testy z prostymi asercjami, których zadaniem jest sprawdzenie działania klasy Order:

class Order {

    private List meals = new ArrayList<>();

    void addMealToOrder(Meal meal) {
        this.meals.add(meal);
    }

    void removeMealFromOrder(Meal meal) {
        this.meals.remove(meal);
    }

    List getMeals() {
        return meals;
    }

    void cancel() {
        this.meals.clear();
    }


}

Powiązaną klasą jest również klasa Meal:

class Meal {

    private int price;
    private String name;

    Meal(int price) {
        this.price = price;
    }

    Meal(int price, String name) {
        this.price = price;
        this.name = name;
    }

}

I załóżmy, że chcielibyśmy napisać kod, który byłby wykonywany przed każdym wywołaniem metody z adnotacją @BeforeEach oraz po każdym wywołaniu metody z adnotacją @AfterEach. Dotyczyłoby to zatem każdego testu jednostkowego w klasie OrderTest.

Żeby to zrobić, musimy najpierw utworzyć nową klasę – BeforeAfterExtension. Ta klasa powinna implementować interfejsy BeforeEachCallback oraz AfterEachCallback (można się posiłkować grafiką powyżej):

public class BeforeAfterExtension implements BeforeEachCallback, AfterEachCallback {

}

Kolejnym krokiem jest zaimplementowanie metod zadeklarowanych w tych interfejsach. Będzie to metoda beforeEach dla interfejsu BeforeEachCallback oraz metoda afterEach dla interfejsu AfterEachCallback.

I żeby niepotrzebnie nie komplikować tego przykładu, w środku tych metod wyświetlimy prosty komunikat:

public class BeforeAfterExtension implements BeforeEachCallback, AfterEachCallback {

    @Override
    public void beforeEach(ExtensionContext extensionContext) {
        System.out.println("Inside before each extension");
    }

    @Override
    public void afterEach(ExtensionContext extensionContext) {
        System.out.println("Inside after each extension");
    }

}

I to tyle! Teraz możemy powrócić do klasy testowej OrderTest.

Jak zastosować dane rozszerzenie?

Aby skorzystać z rozszerzenia, które utworzyliśmy wcześniej, musimy umieścić nad klasą testową specjalną adnotację @ExtendWith. Jako argument tej adnotacji podajemy nazwę naszej klasy rozszerzenia i dodajemy końcówkę .class:

@ExtendWith(BeforeAfterExtension.class)
class OrderTest {

Wówczas takie rozszerzenie będzie stosowane dla całej klasy, a więc dla wszystkich metod testowych. Możemy jednak ograniczyć je tylko do jednej metody. Wtedy adnotacje umieścić należy nad wybraną metodą.

Gdy teraz uruchomimy wszystkie testy z klasy OrderTest, to w konsoli otrzymamy komunikat:

Inside before each extension
Inside after each extension
Inside before each extension
Inside after each extension
Inside before each extension
Inside after each extension
Inside before each extension
Inside after each extension
Inside before each extension
Inside after each extension
Inside before each extension
Inside after each extension

Widać zatem, że para wiadomości:

Inside before each extension
Inside after each extension

została powtórzona sześć razy – tyle ile jest metod testowych w tej klasie. A więc wszystko się zgadza.

Jednak żeby upewnić się, że na pewno metody z naszego rozszerzenia uruchamiają się przed i po metodach z adnotacjami @BeforeEach i @AfterEach, to do nich również dodamy proste komunikaty:

@BeforeEach
void initializeOrder() {
    System.out.println("Before each");
    order = new Order();
}

@AfterEach
void cleanUp() {
    System.out.println("After each");
    order.cancel();
}

Teraz uruchomimy już tylko jeden test – testAssertArrayEquals, żeby nie widzieć wszystkich komunikatów powtórzonych sześciokrotnie:

Inside before each extension
Before each
After each
Inside after each extension

I widzimy, że zgodnie z planem, metody z naszej klasy rozszerzeń – BeforeAfterExtension – były uruchomione odpowiednio przed oraz po metodach z adnotacjami @BeforeEach oraz @AfterEach.

Nie wszystko stracone

Dla osób, które tęsknią za dawną funkcjonalnością Rules, a jednocześnie chcą się przenieść na JUnita 5 – proszę nie panikować! Społeczność programistyczna przygotowała listę silników testowych oraz rozszerzeń, z których można korzystać we własnych projektach: https://github.com/junit-team/junit5/wiki/Third-party-Extensions

Na powyższej liście znajdziemy między innymi pozycję zatytułowaną „JUnit Extensions”: https://glytching.github.io/junit-extensions/index.

Znajduje się tam kilka rozszerzeń, które są odpowiednikami popularnych Rules z JUNita 4. Wśród nich jest także znany nam z początku wpisu TemporaryFolder: https://glytching.github.io/junit-extensions/temporaryFolder

Podsumowanie

Chociaż przykład podany w tym wpisie był dość prosty, to myślę, że dość dobrze pokazuje jak duże możliwości daje Extension Model. I tak naprawdę tylko od nas zależy w jaki sposób zdecydujemy się wykorzystać jego potencjał.

Kod z przykładami do powyższego wpisu znajdziesz na githubie.

The post JUnit 5 – Extension Model appeared first on Dev Foundry.

]]>
http://devfoundry.pl/junit-5-extension-model/feed/ 2
Jaki język programowania wybrać? http://devfoundry.pl/jaki-jezyk-programowania-wybrac/ http://devfoundry.pl/jaki-jezyk-programowania-wybrac/#comments Wed, 17 Oct 2018 07:05:46 +0000 http://devfoundry.pl/?p=779 Jaki język programowania wybrać na początek? Od czego zacząć? Te pytania zadaje sobie każdy, kto chce wstąpić na ścieżkę programistyczną – niezależnie, czy jest to ktoś dopiero zaczynający swoją karierę na rynku pracy lub ktoś, kto chce się przebranżowić. Oczywiście takie pytania są zasadne, bo nikt nie chce tracić czasu na naukę czegoś, co ostatecznie okaże się niepotrzebne w docelowej pracy. Jeśli więc jesteś w jednej albo drugiej grupie osób, to nie martw się – w tym wpisie postaramy się ...

Czytaj dalej...Jaki język programowania wybrać?

The post Jaki język programowania wybrać? appeared first on Dev Foundry.

]]>
Jaki język programowania wybrać na początek? Od czego zacząć? Te pytania zadaje sobie każdy, kto chce wstąpić na ścieżkę programistyczną – niezależnie, czy jest to ktoś dopiero zaczynający swoją karierę na rynku pracy lub ktoś, kto chce się przebranżowić. Oczywiście takie pytania są zasadne, bo nikt nie chce tracić czasu na naukę czegoś, co ostatecznie okaże się niepotrzebne w docelowej pracy. Jeśli więc jesteś w jednej albo drugiej grupie osób, to nie martw się – w tym wpisie postaramy się znaleźć coś właśnie dla Ciebie.

Wybór według popularności

Jest wiele różnych sposobów na wybranie języka programowania. Jeśli interesuje Cię tylko i wyłącznie to, jaki język jest popularny, to istnieje wiele tak zwanych indeksów popularności. Przykładem jest TIOBE: https://www.tiobe.com/tiobe-index/ – indeks aktualizowany miesięcznie, który jest tworzony na podstawie liczby zatrudnionych programistów, wyników wyszukiwań w popularnych przeglądarkach internetowych, serwisach takich jak Wikipedia i YouTube, ilości kursów online, i tak dalej. Oczywiście takie wskaźniki nie pokazują jaki język jest najpopularniejszy w miejscu, w którym aktualnie mieszkasz lub chcesz zamieszkać. Aby się tego dowiedzieć, należałoby przejrzeć konkretne ogłoszenia o pracę.


Jeśli chcesz prześledzić zmiany krok po kroku w formie wideo, to zapraszamy na nasz kanał na YouTube.


Znany i stabilny czy nowy i modny?

Jednak popularność to nie wszystko. Jeśli jakiś język albo framework jest nowy, daje to potencjalnie większą szansę na znalezienie pracy, bo mało osób go zna, ale z drugiej strony może się okazać, że szybko zostanie wyparty przez kolejną nową technologię.

Z kolei znane języki i frameworki mają to do siebie, że są stabilne. I to nie tylko w swoim działaniu, ale też z punktu widzenia danej firmy. Wiadomo, że w sieci znajduje się pełna dokumentacja takiego języka i fora internetowe są pełne pytań i odpowiedzi na różne problemy. Są też wszelkie kursy, filmy i tutoriale. W tym przypadku konkurencja na rynku pracy jest duża, ale jest również dużo ofert pracy i można mieć pewność, że zapotrzebowanie na programistów tego języka nie zniknie ot tak.

Język + framework

Często wraz z językiem programowania powiązana jest jakaś inna technologia: framework (czyli zbiór gotowych rozwiązań programistycznych dla danego języka programowania), biblioteka lub platforma. Na pewno warto je poznać po nauczeniu się podstaw danego języka i z pewnością ułatwi to późniejsze poszukiwania pracy.

Do wyboru do koloru!

Zanim dokonasz wyboru konkretnego języka, warto najpierw rozważyć możliwe dziedziny, w których możesz się realizować jako programista.

Są to:

  • gry,
  • aplikacje desktopowe,
  • aplikacje mobilne,
  • aplikacje webowe,
  • aplikacje embedded,
  • data science.

Gry

 +  +  + 

W przypadku gier sprawa jest dość prosta. Tutaj króluje C++ i bardzo popularny silnik Unreal Engine. Silnik ten jest napisany właśnie w C++ i jest wieloplatformowy, czyli gry docelowo można wydawać na systemy Windows, Linux, MacOS i na konsole.

Poza tym niektóre zespoły deweloperskie programują również w języku C#, korzystając ze środowiska Unity.

Ostatnią opcją jest wykorzystanie języka Java wraz z biblioteką libgdx. Jest to bardziej niszowy wybór, na który decydują się głównie twórcy gier niezależnych. Dzięki libgdx najczęściej tworzy się gry na urządzenia mobilne albo przeglądarki internetowe.

Aplikacja desktopowe

W przypadku aplikacji desktopowych wszystko zależy od docelowej platformy:

   

  • Dla MacOS polecamy język Swift, bo jest teraz bardzo popularny. Jest też objective-c, ale Swift jest jego następcą i opcją znacznie bardziej przyszłościową.

    + 

  • Następnie mamy system Linux, który daje większą elastyczność, bo programować możemy zarówno w C/C++, Javie lub Pythonie. Jednak jeśli mielibyśmy zawęzić ten wybór, to polecili byśmy Javę wraz z platformą JavaFX lub Pythona.

    +  + 

  • Jeśli zaś wybór padnie na Windowsa, swoją przygodę można zacząć z C# i frameworkiem .NET albo z Javą i platformą JavaFX

Aplikacje mobilne

    , 

Przejdźmy do aplikacji mobilnych: tutaj znowu mamy podział na systemy operacyjne:

W przypadku iOS będziemy programować w Swifcie, a w przypadku Androida – w Javie.

Jest też Kotlin, który jest językiem wieloplatformowym, tzn domyślnie możemy go używać do pisania programów na Androida, natomiast przy wykorzystaniu Kotlin Native, można pisać programy, które będą kompilowane do kodu, który można użyć również na iOS i innych systemach.

Aplikacje webowe

Aplikacje webowe są chyba najbardziej popularne i powszechne, zwłaszcza w środowiskach korporacyjnych i generalnie w zastosowaniach komercyjnych. I tu faktycznie jest prawdziwa bonanza języków i technologii.

Wybierając drogę developera aplikacji webowych, najpierw musisz sobie zadać pytanie czy chcesz programować frontend czy backend:

    +  + 

  • Frontend to wszystko to co dzieje się po stronie klienta, czyli przeglądarki internetowej, a więc to co widać na stronie internetowej: design, tekst, grafika, ustawienie poszczególnych elementów. Programista frontendowy (o ile w zespole nie ma UX/UI designera) odpowiada więc za wystrój strony, a także wysyła lub odbiera dane od części backendowej (serwerowej) aplikacji i wyświetla je w odpowiedniej formie na stronie internetowej. W przypadku frontendu powinno się zacząć przygodę od Javascriptu (lub jego nadzbioru: Typescriptu) oraz frameworka Angular albo biblioteki React. Są one w tej chwili najbardziej powszechne.

    +  +  + ,

  • Backend natomiast to wszystko to, co dzieje się po stronie serwera. Odpowiada on za odbiór danych przesłanych (na przykład z formularza) od części frontendowej, ich walidację (czyli sprawdzenie poprawności), ewentualne przetworzenie i najczęściej zapisanie w bazie danych. I w drugą stronę: na odpowiednie zapytanie frontendowe, backend sięga do bazy danych i wyciąga z niej odpowiednie informacje, które następnie zwraca. Tutaj w środowisku korporacyjnym króluje Java wraz z frameworkiem Spring oraz C# wraz z platformą .NET. Natomiast nieco mniej popularne, ale równie istotne będą: Python z frameworkiem Django, Ruby ze słynnym frameworkiem Ruby on Rails czy PHP z frameworkiem Symfony. Ciekawostką może być to, że pojawia się tutaj również Javascript, który w połączeniu ze środowiskiem Node.js może być równie dobrym językiem backendowym, zwłaszcza jeśli chodzi o mniejsze projekty.

    +  +  + 

Systemy wbudowane

Przechodzimy do systemów wbudowanych. Są to systemy, w których programuje się sterowniki różnego rodzaju urządzeń. Mogą to być czujniki temperatury, prędkości przejazdu danego pojazdu lub bardziej przyziemne rzeczy, jak na przykład bankomat, lodówka albo zmywarka. Do programowania tych systemów wykorzystuje się najczęściej języki niskopoziomowe, czyli takie, których instrukcje i funkcje mogą być łatwo tłumaczone na język maszynowy, zrozumiały dla danego urządzenia.

Tu najbardziej popularny będzie język C, ewentualnie C++. Na popularności w tej dziedzinie programowania zyskuje również Python.

Data science

Została nam jeszcze sekcja data science. Jest to dział programowania, który polega na analizie bardzo dużych zbiorów danych. W przypadku data science, oprócz samego programowania będzie też potrzebna znajomość innych dziedzin nauki, a konkretnie matematyki i statystyki. Wymagana będzie również znajomość pewnych algorytmów. Programista data science wychwytuje z ogromnego zbioru pozornie ze sobą nie powiązanych danych pewne zależności, które później są wykorzystywane w różnego rodzaju wizualizacjach: raportach lub wykresach.

Data science obejmuje również takie dziedziny jak sztuczna inteligencja i uczenie maszynowe.

W przypadku data science najpopularniejsze języki programowania to R oraz Python.

Czym jeszcze się kierować?

Oprócz samego zastosowania, to co może Tobą kierować przy wyborze danego języka to jego składnia, dlatego przed podjęciem ostatecznej decyzji, warto przejść kilka tutoriali dla początkujących zanim sięgniesz po pierwszy kurs, film lub książkę do nauki.

Materiały pomocnicze

Polecamy również skorzystanie z serwisu: https://jaki-jezyk-programowania.pl/, który w prosty i przejrzysty sposób, krok po kroku pomoże Ci w wyborze odpowiedniego języka.

Mała firma czy duża korporacja?

Warto również pamiętać, że języki które są popularne w wielkich korporacjach, niekoniecznie znajdą zastosowanie w mniejszych firmach. Jeszcze inaczej rzeczy mają się w przypadku startupów. Tam często kusi się programistów obietnicami pracy w zupełnie nowym projekcie z wykorzystaniem najnowszego języka i frameworka. I programiści znudzeni wieloletnią pracą z jakimś projektem mogą na to przystać. Jednak występuje tu ryzyko, że koniec końców taki projekt nie wypali i za jakiś czas trzeba będzie szukać nowej pracy.

Jak się uczyć?

Natomiast co do sposobu nauki, to jest to temat na cały kolejny wpis i jest to sprawa dość indywidualna, ale obecnie bardzo popularna jest nauka z kursów online, których przybywa coraz więcej. Polecamy tutaj platformę Udemy. A poza tym bardzo dobre są również książki. Ważne tylko, żeby były możliwie jak najnowsze, aby nie uczyć się starych wersji danego języka lub frameworka.

Podsumowanie

Mam nadzieję, że ten poradnik nieco rozjaśnił Ci kwestię wyboru języka programowania. Wybór jest naprawdę spory, ale najważniejsze to po prostu dokonać wyboru i zacząć programować. Idealnie pasuje tutaj powiedzenie: „Praktyka czyni mistrza”.

The post Jaki język programowania wybrać? appeared first on Dev Foundry.

]]>
http://devfoundry.pl/jaki-jezyk-programowania-wybrac/feed/ 1
Wyrażenia lambda i interfejsy funkcyjne http://devfoundry.pl/wyrazenia-lambda-i-interfejsy-funkcyjne/ http://devfoundry.pl/wyrazenia-lambda-i-interfejsy-funkcyjne/#comments Wed, 29 Aug 2018 06:23:35 +0000 http://devfoundry.pl/?p=697 Wyrażenia lambda i interfejsy funkcyjne zostały wprowadzone wraz z Javą 8. Zostały dodane, aby ułatwić życie programistom i zachęcić ich do tworzenia kodu funkcyjnego, a nie imperatywnego. Dzięki ich właściwemu użyciu kod staje się krótszy, bardziej czytelny i przejrzysty. Z perspektywy lat (mijają już ponad 4 lata od wydania Javy 8) można stwierdzić, że wyrażenia lambda oraz interfejsy funkcyjne zostały dobrze przyjęte w środowisku: są używane chętnie i często. Interfejsy funkcyjne Interfejsy funkcyjne zostały wprowadzone w Javie 8, aby umożliwić ...

Czytaj dalej...Wyrażenia lambda i interfejsy funkcyjne

The post Wyrażenia lambda i interfejsy funkcyjne appeared first on Dev Foundry.

]]>
Wyrażenia lambda i interfejsy funkcyjne zostały wprowadzone wraz z Javą 8. Zostały dodane, aby ułatwić życie programistom i zachęcić ich do tworzenia kodu funkcyjnego, a nie imperatywnego. Dzięki ich właściwemu użyciu kod staje się krótszy, bardziej czytelny i przejrzysty. Z perspektywy lat (mijają już ponad 4 lata od wydania Javy 8) można stwierdzić, że wyrażenia lambda oraz interfejsy funkcyjne zostały dobrze przyjęte w środowisku: są używane chętnie i często.

Interfejsy funkcyjne

Interfejsy funkcyjne zostały wprowadzone w Javie 8, aby umożliwić działanie funkcyjne w wyrażeniach lambda.

Definicja takiego interfejsu jest bardzo prosta: interfejsem funkcyjnym jest każdy interfejs, który posiada deklarację tylko i wyłącznie jednej metody abstrakcyjnej. Dla przypomnienia: metoda abstrakcyjna to taka metoda, która nie posiada ciała, czyli definicji. Interfejs funkcyjny może posiadać metody defaultowe albo statyczne. Ważne jednak, aby posiadał tylko i wyłącznie jedną metodę abstrakcyjną.

Klasycznym przykładem takiego interfejsu, znanym jeszcze sprzed Javy 8, jest interfejs Runnable, który posiada deklarację tylko jednej metody: run.

UWAGA: Specjalnym wyjątkiem są metody, które nadpisują metody abstrakcyjne z klasy java.lang.Object takie jak np. equals. Obecność takich metod jest dozwolona w interfejsach funkcyjnych. Za przykład takiego wyjątku może posłużyć opisywany niedawno interfejs Comparator. Jest on interfejsem funkcyjnym, mimo że posiada nie tylko deklarację metody abstrakcyjnej compareTo, ale również deklarację metody equals.

Aby lepiej określić intencję programistyczną, dobrze jest oznaczyć dany interfejs jako funkcyjny za pomocą adnotacji @FunctionalInterface. Taka adnotacja nie spełnia jednak jedynie funkcji dekoracyjno-informującej. Jeśli zostanie umieszczona nad interfejsem, w którym nie znajduje się dokładnie jedna metoda abstrakcyjna, to wystąpi błąd kompilacji.


Jeśli chcesz prześledzić zmiany krok po kroku w formie wideo, to zapraszamy na nasz kanał na YouTube.


Gotowe vs customowe

Wraz z nadejściem konceptu interfejsów funkcyjnych, pojawiła się również nowa paczka: java.function, w której znajdziemy całe mnóstwo „gotowych” interfejsów funkcyjnych, z których możemy skorzystać w wyrażeniach lambda (o których za chwilę). Pojawiają się tutaj interfejsy, których nazewnictwo ma nam podpowiedzieć jakiego rodzaju metoda znajduje się w tym interfejsie. Jest to na przykład:

  • Consumer, który posiada metodę accept(T t). Tu więc sprawa jest dość oczywista: jest to interfejs z metodą, która „konsumuje” dany przekazany argument, coś z nim robiąc, ale nie zwraca niczego. Najprostszym przykładem wykorzystania implementacji takiego interfejsu jest chociażby System.out.println. Nic nie zwraca, za to przyjmuje jeden argument – String i wypisuje go na standardowym wyjściu,
  • Supplier – przeciwieństwo Consumera. Nie przyjmuje żadnych argumentów, za to zwraca wartość danego typu za pomocą metody get(). Przykład Suppliera: new Random().nextInt().

Tych „gotowych” interfejsów jest mnóstwo. Cała lista jest dostępna na stronie Oracle: TUTAJ.

Jeśli jednak chcemy poćwiczyć lub też na powyższej liście nie znajdziemy interfejsu, który będzie odpowiadał naszym potrzebom, to można się również pokusić o napisanie własnego interfejsu funkcyjnego. Na przykład:

@FunctionalInterface
public interface Sum {

    int calculate(int a, int b);

}

Oczywiście sam interfejs nie wystarczy. Potrzebna jest również jego implementacja. I tutaj ponownie mamy wybór: możemy sami napisać klasę implementującą albo skorzystać z dostępnego rozwiązania.

Jak jednak wykorzystać taki interfejs funkcjonalny? Oczywiście w wyrażeniach lambda!

Wyrażenia lambda

Wyrażenia lambda zostały wprowadzone, aby skrócić zapis anonimowych klas wewnętrznych.

Weźmy nasz interfejs Sum. Aby użyć go w danym miejscu możemy albo napisać klasę implementującą metodę calculate, albo utworzyć w wybranym miejscu anonimową klasę wewnętrzną:

int x = 2;
int y = 5;

Sum sum = new Sum() {
    @Override
    public int calculate(int a, int b) {
        return a + b;
    }
};


System.out.println(sum.calculate(2, 5));

Taki zapis nie podobał się jednak wielu osobom, więc postanowiono go zmienić, przy okazji promując bardziej funkcyjne podejście do programowania w Javie.

Na skróty

Skoro mamy już przed znakiem równości podaną nazwę interfejsu, to po co dublować kod po prawej stronie? Usuwamy więc nazwę i nawiasy klamrowe, które tylko niepotrzebnie zajmują miejsce:

Sum sum =
    @Override
    public int calculate(int a, int b) {
        return a + b;
    };

To jednak nie koniec: możemy usunąć też adnotację @Overrride i nazwę metody calculate. I po raz kolejny usuwamy nikomu niepotrzebne nawiasy klamrowe:

Sum sum = (int a, int b) return a + b;

Zostały nam tylko nawiasy okrągłe z deklaracją parametrów i zwracana suma. I to byłoby prawie na tyle, jednak nie musimy podawać typu argumentów, Java się tego domyśli. Równie niepotrzebne jest słowo kluczowe return. Co zatem nam zostanie?

Sum sum = (a, b)  a + b;

I na sam koniec zostało nam jeszcze dodanie takiej symbolicznej „kropki nad i” w postaci strzałki charakterystycznej dla wyrażeń lambda:

Sum sum = (a, b) -> a + b;

I teraz to już wszystko! Ale jak to zapamiętać i się nie pomylić?

Ten skrót działa w bardzo prosty sposób. W interfejsie funkcyjnym mamy deklarację metody abstrakcyjnej, która będzie coś zwracać albo nie. Będzie ona też przyjmować jakieś parametry albo nie.

I analogicznie w naszym zapisie lambda. Można to przedstawić w ten sposób:

(lista parametrów metody) -> ciało metody

Proste, prawda? Parametry, strzałka i ciało lub wynik metody. Jest jeszcze jeden skrót (już ostatni – obiecuję!): jeśli dana metoda abstrakcyjna ma tylko jeden parametr, to możemy się pozbyć również nawiasów okrągłych.

Załóżmy, że mamy taki interfejs:

@FunctionalInterface
public interface Factor {
    
    int calculate(int a);
    
}

Wtedy przykładowy zapis lambda wyglądałby tak:

Factor factor = a -> a * a;

Jest to analogiczne odbicie tego, co chcielibyśmy zdefiniować w anonimowej klasie wewnętrznej, tylko o wiele prostsze.

Interfejsy funkcyjne wraz z wyrażeniami lambda znajdują zastosowanie w przeróżnych metodach. Prostym przykładem jednej z nich jest lista elementów, na której możemy wywołać metodę forEach. Przyjmuje ona jako typ wspomniany wyżej interfejs funkcyjny Consumer:

foreach dev foundry blog programowanie java spring kursy
Metoda forEach przyjmująca interfejs funkcyjny Consumer jako parametr

W tym wypadku możemy zatem użyć znanego nam już Consumera, czyli System.out.println:

integerList.forEach(element -> System.out.println(element));

Wyrażenia lambda mogą również mieć więcej niż jedną linię kodu. Wtedy nie będziemy już w stanie uniknąć nawiasów klamrowych:

integerList.forEach(element -> {
    int x = 5;
    System.out.println("Element: " + element * x);
});

Oczywiście przyzwyczajenie się do takiego zapisu zajmuje trochę czasu, ale serdecznie polecam, bo jest to coś, co dobrze opanowane potrafi zaoszczędzić programiście sporą ilość czasu.

Referencje metod

Referencje metod są kolejnym ułatwieniem, z którego możemy skorzystać, aby zyskać na czasie i czytelności naszego kodu.

Weźmy za przykład interfejs funkcyjny, który ma deklarację metody, która nic nie zwraca i nie przyjmuje żadnych parametrów:

@FunctionalInterface
public interface NothingSpecial {

    void nothing();

}

Teraz przy implementacji tego interfejsu chcielibyśmy wywołać jakąś inną metodę, na przykład taką, która wyświetli prostą wiadomość na ekranie. Korzystając z poznanego już zapisu lambda, piszemy:

public static void main(String[] args) {

    NothingSpecial nothingSpecial = () -> printMessage();

    nothingSpecial.nothing();

}

private static void printMessage() {
    System.out.println("Hello");
}

Zapis jest zatem dość prosty:

  • ( ) puste nawiasy, które jednak muszą się pojawić w sytuacji, gdy metoda interfejsu nie przyjmuje parametrów,
  • -> strzałka,
  • ciało metody, w tym wypadku wywołanie innej metody.

Wciąż są to zatem trzy elementy, które już znamy. A teraz skrót: Jeśli nasz zapis lambda działa tylko jako pośrednik do wywołania innej metody, to zamiast takiego zapisu:

() -> printMessage();

Możemy zapisać:

TestApp::printMessage

Taka przykładowa konstrukcja jest prosta:

nazwa klasy lub obiektu::nazwa metody

Znamienny jest tutaj podwójny dwukropek.

UWAGA: Jedynym wymaganiem jest to, żeby parametry metody abstrakcyjnej interfejsu funkcyjnego i metody w wyrażeniu lambda były takie same (tego samego typu, kolejności i ilości).

Użyjmy teraz referencji metody na powyższym przykładzie listy Integerów, w metodzie forEach. Zapis będzie wyglądał podobnie do tego powyżej:

integerList.forEach(System.out::println);

Istnieją 4 typy referencji metod. I tutaj pozwolę sobie odwołać się do oficjalnej dokumentacji na stronie Oracle, gdzie znajdziemy poniższą tabelę:

Kind Example
Reference to a static method ContainingClass::staticMethodName
Reference to an instance method of a particular object containingObject::instanceMethodName
Reference to an instance method of an arbitrary object of a particular type ContainingType::methodName
Reference to a constructor ClassName::new

Tu zapewne znowu potrzeba będzie trochę czasu i praktyki, aby w pełni przyzwyczaić się do takiego zapisu – ale myślę, że warto.

Podsumowanie

Poznawanie wyrażeń lambda i interfejsów funkcyjnych może być czasochłonne, a na początku również i frustrujące. Na początku trudne może się okazać również zrozumienie jakiegoś dłuższego, wielolinijkowego kodu wyrażenia lambda, którego autorem są nasze koleżanki i koledzy z zespołu. Jednak praktyka i cierpliwość z pewnością zostaną wynagrodzone.

Interfejsy funkcyjne i wyrażenia lambda oraz referencje metod są tutaj po to, aby ułatwić nam życie. Uczmy się więc ich i korzystajmy z nich mądrze, a nasz kod będzie bardziej czytelny, a nasza wydajność programistyczna wzrośnie.

Kod z przykładami do powyższego wpisu znajdziesz na githubie.

The post Wyrażenia lambda i interfejsy funkcyjne appeared first on Dev Foundry.

]]>
http://devfoundry.pl/wyrazenia-lambda-i-interfejsy-funkcyjne/feed/ 5
Sortowanie kolekcji w Javie http://devfoundry.pl/sortowanie-kolekcji-w-javie/ http://devfoundry.pl/sortowanie-kolekcji-w-javie/#respond Wed, 01 Aug 2018 11:06:22 +0000 http://devfoundry.pl/?p=571 Podczas pracy z projektami Javowymi często zdarza się sytuacja, w której musimy posortować daną kolekcję według określonego kryterium. Wtedy z pomocą przychodzą nam dwa interfejsy: Comparable oraz Comparator. W tym wpisie omówię kwestię sortowania kolekcji w Javie, wyjaśnię czym różni się sortowanie przy pomocy interfejsu Comparable od sortowania z wykorzystaniem interfejsu Comparator i w jaki sposób z nich korzystać. Pokażę też dwie kolekcje, których działanie opiera się na zachowaniu odpowiedniej kolejności elementów: TreeSet oraz TreeMap. Sortowanie w kolekcjach Kiedy mówimy o sortowaniu danej ...

Czytaj dalej...Sortowanie kolekcji w Javie

The post Sortowanie kolekcji w Javie appeared first on Dev Foundry.

]]>
Podczas pracy z projektami Javowymi często zdarza się sytuacja, w której musimy posortować daną kolekcję według określonego kryterium. Wtedy z pomocą przychodzą nam dwa interfejsy: Comparable oraz Comparator. W tym wpisie omówię kwestię sortowania kolekcji w Javie, wyjaśnię czym różni się sortowanie przy pomocy interfejsu Comparable od sortowania z wykorzystaniem interfejsu Comparator i w jaki sposób z nich korzystać. Pokażę też dwie kolekcje, których działanie opiera się na zachowaniu odpowiedniej kolejności elementów: TreeSet oraz TreeMap.

Sortowanie w kolekcjach

Kiedy mówimy o sortowaniu danej kolekcji, to najczęściej chodzi nam o skorzystanie z metody sort lub reverse, które są dostępne w klasie Collections, w paczce java.util. Metoda sort sortuje daną kolekcję rosnąco według jakiegoś kryterium, natomiast metoda reverse odwraca kolejność elementów w kolekcji.

Przyjrzyjmy się tym metodom na prostym przykładzie listy Integerów:

List prices = Arrays.asList(1, 5, 32, 76, 3, 21, 7, 333, 43, 221);

System.out.println("Nieposortowane: " + prices);

Collections.sort(prices);

System.out.println("Posortowane rosnąco: " + prices);

Collections.reverse(prices);

System.out.println("Posortowane malejąco: " + prices);

Tworzymy listę liczb całkowitych, wypisujemy na ekran najpierw nieposortowane liczby, następnie  korzystamy z metody sort i wypisujemy elementy kolekcji na ekran – będą one posortowane rosnąco. Następnie wywołujemy metodę reverse i znowu wypisujemy liczby na ekran. Tym razem będą one posortowane malejąco:

Nieposortowane: [1, 5, 32, 76, 3, 21, 7, 333, 43, 221]
Posortowane rosnąco: [1, 3, 5, 7, 21, 32, 43, 76, 221, 333]
Posortowane malejąco: [333, 221, 76, 43, 32, 21, 7, 5, 3, 1]

Oczywiście tyczy się to nie tylko liczb całkowitych, ale również Stringów i ogólnie tak zwanych wrapperów typów prymitywnych, czyli klas typu Integer, Double, Long, Float, etc. W ten sposób możemy również sortować kolekcję przechowującą typy enumowe.

Z pomocy metod sort i reverse możemy skorzystać w każdej kolekcji, która implementuje interfejs List.


Jeśli chcesz prześledzić zmiany krok po kroku w formie wideo, to zapraszamy na nasz kanał na YouTube.


Sortowanie obiektów

Sortowanie obiektów, podobnie jak w przypadku nadpisywania metody equals, jest subiektywne. To od nas zależy według czego będziemy dany obiekt sortować. Możemy bowiem zdecydować się na posortowanie po wartości jednego lub więcej pól. Spójrzmy na przykład akcji na giełdzie:

public class Stock {

    private String name;
    private String symbol;
    private double price;

    public Stock(String name, String symbol, double price) {
        this.name = name;
        this.symbol = symbol;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public String getSymbol() {
        return symbol;
    }

    public double getPrice() {
        return price;
    }

    @Override
    public String toString() {
        return "Stock{" +
                "name='" + name + '\'' +
                ", symbol='" + symbol + '\'' +
                ", price=" + price +
                '}';
    }

}

Klasa posiada trzy pola prywatne, konstruktor, gettery oraz metodę toString, która pozwoli na ładniejsze formatowanie wyświetlanych obiektów tej klasy na standardowym wyjściu.

Gdybyśmy teraz stworzyli kolekcje przechowującą elementy typu Stock i spróbowali na niej wywołać metodę sort, to niestety w linii 11 otrzymamy błąd kompilacji:

Stock stock1 = new Stock("cd projekt blue", "cdp", 15.78);
Stock stock2 = new Stock("amazon", "amz", 5.23);
Stock stock3 = new Stock("microsoft", "mst", 157.65);
Stock stock4 = new Stock("google", "ggl", 7.08);
Stock stock5 = new Stock("apple", "apl", 66.43);

List stockList = Arrays.asList(stock1, stock2, stock3, stock4, stock5);

stockList.forEach(System.out::println);

Collections.sort(stockList);

Nie powinno nas to dziwić: w końcu według jakiego kryterium ta kolekcja miałaby być posortowana? Klasa Stock posiada 4 pola, tak naprawdę możemy sortować listę po każdym z tych pól i w każdym przypadku będzie miało to merytoryczny sens.

Zatem co zrobić? Tutaj pomoże nam właśnie interfejs Comparable.

Interfejs Comparable

W interfejsie Comparable znajduje się deklaracja metody compareTo:

public int compareTo(T o);

Załóżmy, że porównujemy obiekty A i B tego samego typu i na obiekcie A wywołujemy metodę compareTo: A.compareTo(B).

Z opisu metody wynika, że implementacja metody compareTo powinna zwrócić:

  • wartość mniejszą od 0, gdy porównywany obiekt A uznamy za „mniejszy” od obiektu B,
  • wartość większą od 0, gdy porównywany obiekt A uznamy za „większy” od obiektu B,
  • wartość równą 0, gdy obiekt A uznamy za równy obiektowi B.

Zaimplementujmy zatem metodę compareTo w klasie Stock. Załóżmy, że chcemy sortować akcje według ich ceny.

Najpierw dodajemy odpowiednią frazę do deklaracji klasy:

public class Stock implements Comparable {

Teraz czas na implementację metody compareTo. Można to zrobić na kilka sposobów. Najbardziej oczywisty to coś w rodzaju:

@Override
public int compareTo(Stock o) {
    if(this.getPrice() < o.getPrice()) return -1;
    if(this.getPrice() > o.getPrice()) return 1;
    else return 0;
}

Taki zapis można nieco uprościć wykorzystując różnicę matematyczną (z dodatkiem rzutowania w tym konkretnym przypadku):

@Override
public int compareTo(Stock o) {
    return (int) (this.getPrice() - o.getPrice());
}

Kolejnym sposobem jest skorzystanie z metody compare dostępnej w każdym wrapperze typów prymitywnych, o których wspominałem wcześniej. W tym przypadku możemy skorzystać z metody statycznej dostępnej w klasie Double:

@Override
public int compareTo(Stock o) {
    return Double.compare(this.getPrice(), o.getPrice());
}

Skoro mamy już implementację interfejsu Comparable, to możemy teraz sprawdzić czy zadziała nam sortowanie z metodą sort(tam gdzie poprzednio mieliśmy błąd kompilacji):

Stock{name='amazon', symbol='amz', price=5.23}
Stock{name='google', symbol='ggl', price=7.08}
Stock{name='cd projekt blue', symbol='cdp', price=15.78}
Stock{name='apple', symbol='apl', price=66.43}
Stock{name='microsoft', symbol='mst', price=157.65}

Jak widać wszystko działa jak należy: obiekty są posortowane rosnąco według ceny.

Sortowanie po wielu polach

Jednak co zrobić w sytuacji, gdy akcje będą miały taką samą cenę i wtedy w drugiej kolejności chcielibyśmy posortować je według kolejności alfabetycznej po nazwach? A co jeśli nazwy również będą takie same (to się pewnie nie zdarzy na prawdziwej giełdzie :P) i wówczas chcielibyśmy je posortować po symbolach? Nie ma problemu, musimy po prostu dodać do implementacji metody compareTo stosowną logikę:

@Override
public int compareTo(Stock o) {
    int compareResult = Double.compare(this.getPrice(), o.getPrice());
    if(compareResult == 0){
        compareResult = this.getName().compareToIgnoreCase(o.getName());
    }
    if(compareResult == 0){
        compareResult = this.getSymbol().compareToIgnoreCase(o.getSymbol());
    }
    return compareResult;
}

Najpierw porównujemy cenę i wynik przypisujemy do zmiennej compareResult. Następnie sprawdzamy czy compareResult jest równa 0. Jeśli tak, to oznacza to, że obydwa obiekty mają taką samą cenę, więc chcemy je następnie porównać po nazwie. Porównujemy je zatem po nazwie (tym razem korzystając z metody dostępnej w klasie StringcompareToIgnoreCase, która dokona porównania bez względu na wielkość znaków) i ponownie sprawdzamy czy compareResult jest równa 0. Jeśli tak, to zarówno cena, jak i nazwa akcji jest taka sama. Musimy zatem dokonać trzeciego porównania, tym razem symboli. Są one typu String, więc ponownie korzystamy z metody compareToIgnoreCase. Na końcu zwracamy wartość zmiennej compareResult.

W rezultacie, przy drobnej zmianie wartości naszych przykładowych obiektów (tak aby zarówno ceny, jak i nazwy akcji się powtarzały):

Stock stock1 = new Stock("cd projekt blue", "cdp", 5.23);
Stock stock2 = new Stock("amazon", "amz", 5.23);
Stock stock3 = new Stock("amazon", "aat", 5.23);
Stock stock4 = new Stock("google", "ggl", 7.08);
Stock stock5 = new Stock("apple", "apl", 7.08);

otrzymujemy kolekcję akcji poprawnie posortowaną najpierw po cenie, następnie po nazwie i na końcu po symbolu.

CompareToBuilder

Przyszedł czas na poznanie pewnego ułatwienia. Jest nim klasa CompareToBuilder, dzięki której możemy w bardzo prosty sposób zaimplementować metodę compareTo. Klasa ta znajduje się w paczce org.apache.commons.lang3.builder, więc żeby móc z niej skorzystać, należy dodać odpowiednią zależność do pliku pom.xml. Jak się można domyślić, klasa ta jest implementacją wzorca projektowego Builder.

Spójrzmy teraz w jaki sposób prezentowałaby się metoda compareTo, gdybyśmy chcieli ją zaimplementować przy użyciu CompareToBuildera, mając te same wymagania co w powyższym przykładzie (sortowanie po cenie, nazwie i symbolu):

@Override
public int compareTo(Stock o) {
    return new CompareToBuilder()
            .append(this.getPrice(), o.getPrice())
            .append(this.getName(), o.getName())
            .append(this.getSymbol(), o.getSymbol())
            .build();
}

Moim zdaniem wygląda świetnie: prosto i przede wszystkim przejrzyście. Wykonujemy tutaj dość powszechne chainowanie metod. Korzystamy z konstruktora, aby utworzyć obiekt typu CompareToBuilder, używamy metod append, aby dodać kolejne pola według których chcemy sortować obiekty (może to być jedno lub więcej pól) i na końcu wywołujemy metodę build. Całą resztę załatwia za nas CompareToBuilder.

Klasa Comparator

Kolejnym sposobem na sortowanie obiektów w kolekcji jest skorzystanie z interfejsu Comparator. Możemy to zrobić tworząc odrębną klasę, na przykład StockComparator:

public class StockComparator implements Comparator {

    @Override
    public int compare(Stock o1, Stock o2) {
        return new CompareToBuilder()
                .append(o1.getPrice(), o2.getPrice())
                .append(o1.getName(), o2.getName())
                .append(o1.getSymbol(), o2.getSymbol())
                .build();
    }

}

Jest to dość prosta implementacja: tworzymy nową klasę, implementujemy interfejs Comparator (który przyjmuje typ generyczny – podajemy tam oczywiście klasę Stock) i nadpisujemy metodę compare, której sposób działania powinien być identyczny jak metody compareTo z interfejsu Comparable – zatem tu również możemy skorzystać z klasy CompareToBuilder.

Teraz podczas sortowania możemy podać instancję klasy StockComparator jako drugi argument metody sort:

Collections.sort(stockList, new StockComparator());

Możemy również utworzyć instancję klasy typu Comparator jako anonimową klasę wewnętrzną, czyli w żądanym miejscu w kodzie tworzymy od razu instancję oraz definicję całej klasy. Tak utworzoną instancję przekazujemy następnie do metody sort i w pętli wypisujemy elementy kolekcji stockList na ekran:

Comparator stockComparator = new Comparator() {
    @Override
    public int compare(Stock o1, Stock o2) {
        return new CompareToBuilder()
                .append(o1.getPrice(), o2.getPrice())
                .append(o1.getName(), o2.getName())
                .append(o1.getSymbol(), o2.getSymbol())
                .build();
    }
};

Collections.sort(stockList, stockComparator);

stockList.forEach(System.out::println);

Comparator jest interfejsem funkcjonalnym, więc możemy skorzystać z wyrażenia lambda, aby jeszcze bardziej skrócić ten zapis:

Comparator stockComparator = (o1, o2) -> new CompareToBuilder()
        .append(o1.getPrice(), o2.getPrice())
        .append(o1.getName(), o2.getName())
        .append(o1.getSymbol(), o2.getSymbol())
        .build();

Kolejną możliwością, jaką daje nam Java 8 i jej nowsze wersje, jest użycie Comparatora w metodzie interfejsu Streamsorted:

List sortedStocks = stockList.stream()
        .sorted(stockComparator)
        .collect(Collectors.toList());

sortedStocks.forEach(System.out::println);

Jeśli jesteśmy przy strumieniach, to warto poznać dwie przydatne metody z interfejsu Comparator: comparing i thenComparing. Możemy z nich skorzystać używając wywoływań łańcuchowych oraz referencji metod, a wszystko to w obrębie metody stream.sorted:

List sortedStocks = stockList.stream()
        .sorted(Comparator
                .comparing(Stock::getPrice)
                .thenComparing(Stock::getName)
                .thenComparing(Stock::getSymbol))
        .collect(Collectors.toList());

W przypadku Comparatora możemy również skorzystać z metody sort dostępnej bezpośrednio w interfejsie List – jest to alternatywa dla metody Collections.sort, która jako argument przyjmuje właśnie klasę typu Comparator:

stockList.sort(stockComparator);

Kiedy korzystać z klasy typu Comparator?

Klasa typu Comparator będzie szczególnie pomocna gdy:

  • nie mamy bezpośredniego dostępu do klasy, której wartości pól chcemy porównywać, i w związku z tym nie jesteśmy w stanie dodać do niej metody compareTo. Wówczas możemy utworzyć osobną klasę typu Comparator lub utworzyć anonimową klasę wewnętrzną w stosownym miejscu,
  • mamy dostęp do klasy, ale implementuje ona już interfejs Comparable, a my musimy posortować obiekty tej klasy według innego kryterium.

TreeSet i TreeMap

W Javie istnieją również dwie kolekcje, których działanie opiera się między innymi na sortowaniu elementów: TreeSet oraz TreeMap.

TreeSet

Kolekcja TreeSet to zbiór, który implementuje interfejs SortedSet. Elementy tej kolekcji będą sortowane według ich naturalnej kolejności (np. w przypadku elementów typu Integer, będą one uporządkowane rosnąco) lub w kolejności podanej przy tworzeniu zbioru. Mamy dwie możliwości określenia kolejności sortowania elementów:

  • jeśli klasa, z której instancji są złożone elementy zbioru, implementuje interfejs Comparable , to elementy tego zbioru zostaną posortowane według kolejności określonej przez metodę compareTo w rzeczonej klasie,
  • przy deklaracji zbioru możemy w konstruktorze podać klasę typu Comparator i wtedy elementy zbioru będą posortowane zgodnie z logiką określoną w zaimplementowanej metodzie compare.

Przykład tych dwóch implementacji kolekcji TreeSet:

Bez Comparatora w konstruktorze – w tym przypadku klasa Stock musi implementowac interfejs Comparable i nadpisywać metodę compareTo:

TreeSet stockTreeSet1 = new TreeSet<>();

stockTreeSet1.add(stock1);
stockTreeSet1.add(stock2);
stockTreeSet1.add(stock3);
stockTreeSet1.add(stock4);
stockTreeSet1.add(stock5);

stockTreeSet1.forEach(System.out::println);

Oraz z przekazaną instancją klasy typu Comparator w konstruktorze (korzystamy z utworzonej wcześniej instancji klasy typu Comparator i przekazujemy ją w konstruktorze):

TreeSet stockTreeSet2 = new TreeSet<>(stockComparator);

stockTreeSet2.add(stock1);
stockTreeSet2.add(stock2);
stockTreeSet2.add(stock3);
stockTreeSet2.add(stock4);
stockTreeSet2.add(stock5);

stockTreeSet2.forEach(System.out::println);

TreeMap

Dość podobnie sprawy mają się w przypadku kolekcji TreeMap. Jest to mapa implementująca interfejs SortedMap. Jej klucze sortowane są według naturalnej kolejności, chyba że kluczami są obiekty klasy, która implementuje interfejs Comparable lub jeśli przy deklaracji mapy, w konstruktorze podamy Comparator:

TreeMap stockStringTreeMap = new TreeMap<>(stockComparator);

stockStringTreeMap.put(stock1, "stock 1");
stockStringTreeMap.put(stock2, "stock 2");
stockStringTreeMap.put(stock3, "stock 3");
stockStringTreeMap.put(stock4, "stock 4");
stockStringTreeMap.put(stock5, "stock 5");

stockStringTreeMap.keySet().forEach(System.out::println);

Podsumowanie

Jak wynika z powyższego wpisu – jest wiele różnych sposobów na sortowanie elementów kolekcji w Javie. To jakiego wyboru dokonamy, będzie najczęściej determinowane przez projekt w którym pracujemy, obiekty z jakimi mamy do czynienia, wydajność danego rozwiązania lub jeszcze inne czynniki. Najważniejsze żeby zdawać sobie sprawę z wachlarza dostępnych rozwiązań. Wówczas nic nie będzie nas w stanie zaskoczyć.

Kod z przykładami do powyższego wpisu znajdziesz na githubie.

The post Sortowanie kolekcji w Javie appeared first on Dev Foundry.

]]>
http://devfoundry.pl/sortowanie-kolekcji-w-javie/feed/ 0
Enum – ograniczenia i możliwości http://devfoundry.pl/enum-ograniczenia-i-mozliwosci/ http://devfoundry.pl/enum-ograniczenia-i-mozliwosci/#comments Wed, 04 Jul 2018 06:57:52 +0000 http://devfoundry.pl/?p=475 Enum to specyficzna klasa, która w języku Java reprezentuje specjalny typ wyliczeniowy. Pierwszy raz pojawił się w Javie 5 i został wprowadzony, aby umożliwić programistom lepszą reprezentację zbioru stałych wartości. Posiada zarówno wiele ciekawych właściwości, jak i możliwości do rozwoju, co z kolei powoduje, że może stać się bardzo przydatną klasą. Prosta implementacja klasy Enum Zacznijmy od przykładu, który ilustruje najprostsze, a jednocześnie chyba najczęściej spotykane wykorzystanie klasy Enum w codziennej pracy: public enum TicketType { BUG, TASK, SUGGESTION; } ...

Czytaj dalej...Enum – ograniczenia i możliwości

The post Enum – ograniczenia i możliwości appeared first on Dev Foundry.

]]>
Enum to specyficzna klasa, która w języku Java reprezentuje specjalny typ wyliczeniowy. Pierwszy raz pojawił się w Javie 5 i został wprowadzony, aby umożliwić programistom lepszą reprezentację zbioru stałych wartości. Posiada zarówno wiele ciekawych właściwości, jak i możliwości do rozwoju, co z kolei powoduje, że może stać się bardzo przydatną klasą.

Prosta implementacja klasy Enum

Zacznijmy od przykładu, który ilustruje najprostsze, a jednocześnie chyba najczęściej spotykane wykorzystanie klasy Enum w codziennej pracy:

public enum TicketType {

BUG,
TASK,
SUGGESTION;

}

Zgodnie z konwencją, poszczególne wartości zapisujemy wielkimi literami, oddzielając je przecinkami, a po ostatniej wartości dodajemy średnik.

Mamy teraz zbiór trzech wartości z przykładowego systemu zarządzania projektem, w którym są między innymi taski, bugi i sugestie.

Możemy teraz po prostu utworzyć zmienną typu TicketType w naszej klasie testowej, w metodzie main:

TicketType ticketType = TicketType.TASK;
System.out.println(ticketType);

i wypisać wybraną wartość na standardowym wyjściu. Otrzymamy wartość: TASK.


Jeśli chcesz prześledzić zmiany krok po kroku w formie wideo, to zapraszamy na nasz kanał na YouTube.


Ukryte właściwości i ograniczenia klasy Enum

Zanim zaczniemy nieco rozbudowywać klasę TicketType z naszego przykładu, warto byłoby bliżej przyjrzeć się samej klasie Enum. Czy ma jakieś ukryte właściwości? Jakie są jej ograniczenia?

Zaczniemy od podstawowej rzeczy, czyli definicji klasy typu Enum:

public abstract class Enum>
implements Comparable, Serializable

Każda klasa typu Enum będzie niejawnie rozszerzała klasę Enum (Enum.java z paczki java.lang) oraz implementowała interfejsy Comparable oraz Serializable. Więc mamy tutaj od razu domyślnie zapewnione sortowanie oraz serializację.

Poza tym nie można utworzyć instancji tej klasy. Jeśli będziemy chcieli zapisać na przykład TicketType tType = new TicketType();, to otrzymamy błąd kompilacji. Nie będziemy w stanie również w środku klasy typu Enum utworzyć konstruktora publicznego lub typu protected, ponieważ język Java tego nie przewiduje. Jeśli natomiast utworzymy konstruktor z widocznością domyślną, to później i tak zostanie on zamieniony na konstruktor prywatny.

Tworzy nam się teraz dość jasny obraz klasy typu Enum. W skrócie: nie można tworzyć instancji tej klasy i nie może ona rozszerzać żadnej innej klasy. Można natomiast implementować inne interfejsy, o czym przekonamy się w dalszej części tego wpisu. W enumie możemy również definiować metody oraz dodawać do niego pola.

Ważne jest natomiast to, w jaki sposób tworzone są instancje danego enuma. Otóż działa to w ten sposób, że na każdą wartość umieszczoną w klasie typu Enum, zostanie utworzona jedna instancja enuma, która będzie przechowywać właśnie tę wartość. Enum danego typu nie będzie miał innych instancji poza tymi, które są zdefiniowane przez jego wartości. Oprócz tego zostały dodane dodatkowe mechanizmy zabezpieczające:

  • metoda clone jest typu final, aby nikt nie mógł sklonować danej instancji enuma,
  • tworzenie instancji enuma za pomocą refleksji jest niemożliwe,
  • mechanizm deserializacji dodaje zabezpieczenia, aby nie można było utworzyć kolejnych instancji enumów przy procesie deserializacji.

Sortowanie i kolejność enumów

Oprócz tego, enumowe wartości są domyślnie posortowane według kolejności, w jakiej są umieszczone w klasie typu Enum. Dzieje się tak, ponieważ wewnętrznie każda wartość posiada liczbę integer, korespondującą z jej miejscem w danym enumie, licząc od 0. Na bazie naszego przykładu:

enum dev foundry blog programowanie java spring kursy
Wizualizacja enuma TicketType wraz z wartościami i korespondującymi cyframi ordinal

Wartość BUG została umieszczone w TicketType jako pierwsza, więc jej ukryty numer oznaczający kolejność to 0, TASK to 1, a SUGGESTION to 2. I tak dalej…

Aby wypisać numery kolejności wartości  z danego enuma, wystarczy wywołać metodę ordinal:

for (TicketType ticketType : TicketType.values()) {
    System.out.println(ticketType.ordinal());
}

Przy okazji: aby iterować po wartościach danego enuma, dobrze jest to robić z pomocą metody values (lub za pomocą kolekcji, o czym później).

Singleton

Poznaliśmy już pewne ograniczenia i właściwości klasy typu Enum. Dość jasnym staje się, że twórcy języka starali się, aby uniemożliwić użytkownikowi tworzenie więcej niż jednej instancji danego enuma. Brzmi jak idealny przykład wzorca projektowego Singleton, gdzie potrzebna jest tylko i wyłącznie jedna instancja danej klasy. I faktycznie, możemy użyć enuma do utworzenia Singletona:

public enum SingletonExample {

INSTANCE;

}

Wystarczy umieścić w takim enumie jedną wartość – konwencja mówi, żeby nazwać ją INSTANCE i już. Oczywiście możemy dalej rozbudowywać takiego enuma o dodatkowe pola oraz metody, aby zwiększyć jego możliwości. Jednak już tutaj widać, że jest on znacznie bardziej zaskakujący, niż mogłoby się wydawać na pierwszy rzut oka.

Pola w klasie Enum

Do naszego prostego enuma spróbujemy teraz dodać jakieś pole, na przykład priority:

public enum TicketType {

    BUG("high"),
    TASK("medium"),
    SUGGESTION("low");

    private String priority;

    TicketType(String priority) {
        this.priority = priority;
    }

    public String getPriority() {
        return priority;
    }

    public void setPriority(String priority) {
        this.priority = priority;
    }

}

Jak widać co nieco się tutaj pozmieniało. Dodaliśmy pole prywatne typu String, konstruktor z domyślnym modyfikatorem dostępu oraz getter dla nowego pola.

Ponadto przy wartościach pojawiły się również odpowiedniki pola priority umieszczone w nawiasach. Właśnie w ten sposób deklaruje się pola w klasach typu enum.

Ważna uwaga: Konstruktor w klasie enum nie może być publiczny ani typu protected. Jeśli natomiast dodamy konstruktor z domyślną widocznością, to i tak zostanie on zamieniony na typ prywatny. O tym dlaczego tak się dzieje powiem później.

Teraz w klasie testowej możemy wypisać priorytet poszczególnych wartości. Tym razem zrobimy to w pętli, korzystając z dostępnej w klasie Enum metody values():

for (TicketType ticketType : TicketType.values()) {
    System.out.println(ticketType.getPriority());
}

Na wyjściu otrzymamy:

high
medium
low

Wartości pól można ustawić nie tylko przy pomocy konstruktora – w naszym przykładzie mamy również dostęp do settera pola priority. Dzięki temu możemy zmienić wartość tego pola, nawet jeśli została już wcześniej ustawiona przez konstruktor:

TicketType task = TicketType.TASK;
task.setPriority("very high");

System.out.println(task.getPriority());

I w wyniku otrzymamy oczywiście komunikat: very high.

Metody w klasie Enum

Poza deklaracją pól, w klasach typu Enum możemy również definiować metody. Stwórzmy sobie zatem metodę isAssigned, która jest typowo pokazowa (wiadomo, że samego typu nie będziemy nigdzie przypisywać):

public boolean isAssigned() {
    return false;
}

Przy wypisaniu w pętli powyższej metody, otrzymamy komunikat:

false
false
false

Nie jest to zbyt użyteczne, na szczęście dla wszystkich lub też tylko wybranych wartości możemy nadpisać tę metodę. Zapisuje się to w następujący sposób:

BUG("high"),
TASK("medium"){
    @Override
    public boolean isAssigned() {
        return true;
    }
},
SUGGESTION("low");

I teraz w wyniku otrzymamy:

false
true
false

W enumach możemy również deklarować metody abstrakcyjne, jednak wtedy wszystkie wartości muszą tę metodę implementować.

Implementowanie interfejsów

Ze względu na swoje ograniczenia, klasy typu Enum mogą w zasadzie jedynie implementować inne interfejsy. Utwórzmy zatem na potrzeby naszego przykładu interfejs Commented, który będzie zawierał deklarację jednej metody – comment:

public interface Commented {
    
    void comment();

}

I teraz zaimplementujmy powyższą metodę w TicketType. Cała klasa po zmianach prezentuje się następująco:

public enum TicketType implements Commented {

    BUG("high"),
    TASK("medium"){
        @Override
        public boolean isAssigned() {
            return true;
        }
    },
    SUGGESTION("low");

    private String priority;

    TicketType(String priority) {
        this.priority = priority;
    }

    public boolean isAssigned() {
        return false;
    }

    public void comment() {
        System.out.println("Standard comment");
    }

    public String getPriority() {
        return priority;
    }
    
}

W przypadku metody z interfejsu (podobnie jak w przypadku zwykłych metod), mamy możliwość:

  • implementacji, z której korzystać będą wszystkie wartości enuma,
  • implementacji w wartościach, które nas interesują – w tym wypadku trzeba będzie jednak nadpisać metodę comment.

Skorzystamy z tego drugiego przypadku – nadpiszemy metodę comment dla wartości TASK, aby pokazać jak wygląda składnia, gdy nadpisujemy dwie lub więcej metod:

BUG("high"),
TASK("medium"){
    @Override
    public boolean isAssigned() {
        return true;
    }
    @Override
    public void comment() {
        System.out.println("Comment for a task");
    }
},
SUGGESTION("low");

Metoda System.out.println wypisze nam wówczas:

Standard comment
Comment for a task
Standard comment

Porównywanie enumów

Jak najlepiej porównać dwie wartości z tej samej klasy Enum lub też dwie wartości z różnych enumów?

Java jasno określa sposób porównywania dwóch wartości typu Enum. Wykonuje się to poprzez podwójny znak równości:

TicketType bug = TicketType.BUG;
TicketType suggestion = TicketType.SUGGESTION;

System.out.println(bug == suggestion);

Oczywiście w tym wypadku otrzymamy w wyniku: false. Gdybyśmy jednak uparli się (być może natchnieni moim poprzednim wpisem), aby użyć tutaj metody equals:

System.out.println(bug.equals(suggestion));

to w odpowiedzi otrzymamy również: false, jednak po przejściu do implementacji metody equals w tym wypadku znajdziemy się w klasie Enum.java, w której zobaczymy:

public final boolean equals(Object other) {
    return this==other;
}

Jak widać więc, nie ma co kombinować tutaj z metodą equals i dobrą praktyką jest aby w przypadku enumów porównywać je ze sobą za pomocą podwójnego znaku równości, zgodnie z założeniami języka Java.

Co natomiast z porównywaniem różnych typów enumów? Tutaj sprawa jest jeszcze prostsza, ponieważ kompilator nie pozwoli nam na zastosowanie podobnego zapisu:

TicketType ticketType = TicketType.BUG;
OtherType otherType = OtherType.FIRST;

System.out.println(ticketType == otherType);

Otrzymamy błąd kompilacji mówiący o tym, że mamy do czynienia z niekompatybilnymi typami.

EnumSet i EnumMap

Istnieją dwie kolekcje, które są powiązane z enumami: EnumSet oraz EnumMap.

EnumSet

EnumSet to wyspecjalizowana implementacja zbioru w Javie, która jest specjalnie przystosowana do działania na enumach, dzięki czemu jest bardzo wydajna.

Aby utworzyć zbiór wartości enumowych, możemy skorzystać z kilku metod klasy EnumSet, na przykład:

Set ticketTypeEnumSet = EnumSet.of(TicketType.SUGGESTION, TicketType.BUG);

Wykorzystanie metody of jest przydatne, gdy chcemy utworzyć zbiór składający się niekoniecznie ze wszystkich wartości danego enuma.

Jeśli jednak chcielibyśmy utworzyć zbiór, który faktycznie ma zawierać wszystkie elementy danej klasy Enum, to możemy użyć metody allOf:

Set ticketTypeEnumSet2 = EnumSet.allOf(TicketType.class);

ticketTypeEnumSet2.forEach(System.out::println);

Jak widać, musimy tutaj określić typ enuma z rozszerzeniem .class.

Dzięki umieszczeniu wartości enumowych w kolekcji tego typu, możemy również skorzystać z dobrodziejstw Javy 8, by w prosty sposób iterować po elementach zbioru.

EnumSet implementuje interfejs Set, więc mamy tu do dyspozycji wiele przydatnych metod, takich jak: add, remove, contains, etc.

Istotną informacją jest również to, że EnumSet to klasa abstrakcyjna, która posiada dwie implementacje: RegularEnumSet oraz JumboEnumSet. To, która konkretna klasa zostanie wybrana, jest ustalane przez wirtualną maszynę Javy podczas tworzenia instancji enumów, w zależności od tego ile wartości posiada dana klasa Enum. Jeśli wartości będzie więcej niż 64, to użyty zostanie JumboEnumSet, w przeciwnym wypadku – i najczęściej – będziemy mieli do czynienia z RegularEnumSetem.

EnumMap

Podobnie jak w przypadku EnumSeta, EnumMap jest to kolekcja stworzona specjalnie do pracy z enumami. Ma zapewniać lepszą wydajność i być optymalnym wyborem dla programisty, który potrzebuje przechować wartości enumowe w jakiejś kolekcji.

W tym przypadku dokumentacja Javy mówi o tym, że EnumMap ma przyjmować jako klucz wartości Enum jednego typu, czyli pochodzące z tej samej klasy Enum. Wartości mapy mogą być dowolnego typu. Popatrzmy na prosty przykład:

EnumMap enumMap = new EnumMap(TicketType.class);

enumMap.put(TicketType.TASK, "Dawid Nowak");
enumMap.put(TicketType.BUG, "Paweł Cwik");

System.out.println(enumMap.get(TicketType.BUG));

Działa to identycznie jak HashMapa, do której jesteśmy przyzwyczajeni. Jedyną ciekawostką jest to, że podczas deklaracji tej mapy musimy w konstruktorze podać typ Enuma, który będzie użyty jako klucz, podając również rozszerzenie .class.

Podsumowanie

Jak widać klasa Enum posiada sporo ciekawych właściwości oraz ukrytych tajemnic, które – mam nadzieję – teraz stały się już nieco jaśniejsze. To dobry przykład na to, że możemy użyć tej klasy w jej najmniejszej, minimalistycznej formie, jako sposób na przechowywanie stałych wartości. Jednak jeśli mamy taką możliwość lub potrzebę, to enum oferuje nam również możliwość implementowania interfejsów, definiowania metod i pól, a nawet pozwala na utworzenie klasy typu Singleton.

Kod z przykładami do powyższego wpisu znajdziesz na githubie.

The post Enum – ograniczenia i możliwości appeared first on Dev Foundry.

]]>
http://devfoundry.pl/enum-ograniczenia-i-mozliwosci/feed/ 2