Czytaj dalej...Heap, Stack i String Pool w Javie
The post Heap, Stack i String Pool w Javie appeared first on Dev Foundry.
]]>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.
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 
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:
*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 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.
]]>Czytaj dalej...Słowo kluczowe Static w Javie
The post Słowo kluczowe Static w Javie appeared first on Dev Foundry.
]]>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.
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).
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.
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;
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.
]]>Czytaj dalej...Wzorzec projektowy Fasada
The post Wzorzec projektowy Fasada appeared first on Dev Foundry.
]]>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:
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.
]]>Czytaj dalej...Testy jednostkowe – Mocki
The post Testy jednostkowe – Mocki appeared first on Dev Foundry.
]]>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.
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 JUnit – mockito-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.
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.
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 Mockito – mock. 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?
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ć!
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 ListprepareAccountData() { 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:

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.
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.
]]>Czytaj dalej...Testy jednostkowe – Stuby
The post Testy jednostkowe – Stuby appeared first on Dev Foundry.
]]>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.
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.
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.
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.
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:
ListgetAllActiveAccounts() { 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.
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.
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.
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.
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.
]]>Czytaj dalej...JUnit 5 – Extension Model
The post JUnit 5 – Extension Model appeared first on Dev Foundry.
]]>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:

I tutaj, jak widać, korzystamy z gotowych, zdefiniowanych metod i możemy to robić w dowolnej ilości klas testowych bez ryzyka powtarzania kodu.
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:
Lub jeśli ktoś woli uproszczoną wersję graficzną:

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.
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.
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.
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
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.
]]>Czytaj dalej...Jaki język programowania wybrać?
The post Jaki język programowania wybrać? appeared first on Dev Foundry.
]]>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ę.
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.
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.
Zanim dokonasz wyboru konkretnego języka, warto najpierw rozważyć możliwe dziedziny, w których możesz się realizować jako programista.
Są to:
+
+
, 
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.
W przypadku aplikacji desktopowych wszystko zależy od docelowej platformy:
: 
:
, 
:
, 
:
,
: 
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 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:
+
,
+ 
,
,
+
,
+
+
+ 
, 
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.
, 
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.
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.
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.
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.
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.
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.
]]>Czytaj dalej...Wyrażenia lambda i interfejsy funkcyjne
The post Wyrażenia lambda i interfejsy funkcyjne appeared first on Dev Foundry.
]]>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.
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 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.
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:

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 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:
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.
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.
]]>Czytaj dalej...Sortowanie kolekcji w Javie
The post Sortowanie kolekcji w Javie appeared first on Dev Foundry.
]]>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.
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:
Listprices = 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.
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.
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ć:
A uznamy za „mniejszy” od obiektu B,A uznamy za „większy” od obiektu B,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.
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 String: compareToIgnoreCase, 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.
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.
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:
ComparatorstockComparator = 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:
ComparatorstockComparator = (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 Stream – sorted:
ListsortedStocks = 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:
ListsortedStocks = 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);
Klasa typu Comparator będzie szczególnie pomocna gdy:
compareTo. Wówczas możemy utworzyć osobną klasę typu Comparator lub utworzyć anonimową klasę wewnętrzną w stosownym miejscu,W Javie istnieją również dwie kolekcje, których działanie opiera się między innymi na sortowaniu elementów: TreeSet oraz TreeMap.
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:
compareTo w rzeczonej klasie,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:
TreeSetstockTreeSet1 = 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):
TreeSetstockTreeSet2 = new TreeSet<>(stockComparator); stockTreeSet2.add(stock1); stockTreeSet2.add(stock2); stockTreeSet2.add(stock3); stockTreeSet2.add(stock4); stockTreeSet2.add(stock5); stockTreeSet2.forEach(System.out::println);
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:
TreeMapstockStringTreeMap = 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);
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.
]]>Czytaj dalej...Enum – ograniczenia i możliwości
The post Enum – ograniczenia i możliwości appeared first on Dev Foundry.
]]>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.
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:
clone jest typu final, aby nikt nie mógł sklonować danej instancji enuma,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:

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).
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.
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.
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ć.
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ść:
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
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.
Istnieją dwie kolekcje, które są powiązane z enumami: EnumSet oraz EnumMap.
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:
SetticketTypeEnumSet = 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:
SetticketTypeEnumSet2 = 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.
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:
EnumMapenumMap = 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.
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.
]]>