Jak dobre są Twoje testy? Czyli kilka słów o testowaniu mutacyjnym.

Załóżmy, że mamy pewną aplikacje, o którą bardzo się troszczymy i robimy wszystko, żeby zapewnić jak najlepszą jakość kodu tejże aplikacji. Pokrycie kodu testami jednostkowymi jest jednym ze sposobów, którego możemy użyć, aby ‚zabezpieczyć’ nasz kod przed błędami. Dwoimy się i troimy, aż w końcu udaję nam się pokryć nasz kod w 100% i teraz mamy pewność, że żaden bug nie wkradł się do naszej aplikacji! Ale czy na pewno?

Stwórzmy aplikacje, o którą zadbamy!

Zacznijmy od ‚zewnętrznego’ serwisu, który będziemy wstrzykiwać do naszej klasy.

Teraz stwórzmy główną klasę naszej aplikacji:

Jak widzimy, nie ma tutaj żadnego ‚Rocket Science’ 🙂 Pobieramy z zewnętrznego serwisu dwie liczby, a następnie je porównujemy. Jeżeli pierwsza z nich jest większa to odejmujemy od niej drugą liczbę i otrzymaną liczbę zwracamy jako wynik. W przeciwnym przypadku jako rezultat zwracamy sumę dwóch pobranych liczb.

Pora przetestować naszą aplikacje!

Stwórzmy najpierw pomocniczą klasę, która będzie implementować interfejs naszego zewnętrznego serwisu.

I teraz pierwsza metoda testowa:

W powyższej metodzie testowej sprawdziliśmy pierwszy przypadek, czyli sytuacje gdzie pierwsza pobrana liczba z zewnętrznego serwisu jest większa od kolejnej.

Teraz pora na drugi przypadek:

Tutaj pokryliśmy drugi przypadek, czyli sytuacje gdy druga liczba jest większa od pierwszej.

Wygląda na to, że pokryliśmy wszystkie ‚branche’ i nasz kod jest w pełni przetestowany, ale skąd mamy wiedzieć jak dobre są nasze testy jednostkowe?

Testy mutacyjne

Tutaj w pomocą przychodzą nam testy mutacyjne. Czym zatem są owe testy? Jest to technika polegająca na wprowadzaniu małych i losowych zmian w kodzie naszej aplikacji. Zmiany te powinny zostać wykryte przez nasze testy jednostkowe. Jeżeli, któraś ze zmian nie została wykryta oznacza to, że nasze testy mogą nie być tak dobre jak nam się wydawało 😉

Jakie zmiany?

Poniżej znajduję się lista z przykładowymi zmianami, które mogą zostać wprowadzone w naszym kodzie.

  • Zmiana granicy w warunkach, np. > zostanie zmienione na >=, >= na >, itd.
  • Negacja warunków, np. == zostanie zmienione na !=, <= na >, itd.
  • Usunięcie warunków i zastąpienie ich stałą wartością, np. a > b zostanie zmienione na true
  • Zmiana operacji matematycznych, np. dodawanie zostanie zamienione na odejmowanie, a mnożenie na dzielenie
  • Zmiana wartości zmiennych na wartości defaultowe lub stałe, np. int zostanie ustawiony na 0 lub inną losową wartość
  • Zwrócenie null zamiast obiektu
  • Pominięcie wywołania metody typu void

Właśnie zapoznaliśmy się z przykładowymi modyfikacja, które mogą zostać wprowadzone do naszej aplikacji podczas testów mutacyjnych. Nasze testy jednostkowe powinny być napisane w taki sposób, aby zmiany te spowodowały to, że nasze testy nie przejdą.

Testy mutacyjne w praktyce

Wróćmy teraz do naszego kodu, który napisaliśmy na początku i spróbujmy przeprowadzić testy mutacyjne. Z pomocą przyjdzie nam biblioteka PIT!

Konfiguracja

Konfiguracja i uruchomienie PIT są banalnie proste! Pierwsze co musimy zrobić to dodać plugin do naszego poma:

Domyślnie wszystkie klasy z naszej aplikacji zostaną poddane testom mutacyjnym. Jeżeli chcemy to zmienić to możemy skonfigurować pakiety klas/testów, które będą wzięte pod uwagę.

Uruchomienie

Aby przeprowadzić testy mutacyjne wystarczy wywołać następujące polecenie:

Gdy operacja zakończy się sukcesem zostanie wygenerowany raport z wynikami. Znajduje się on pod następującą ścieżką: target/pit-reports/yyyyMMddHHmm.

Zmutujmy naszą aplikacje!

Pora wrócić do naszej aplikacji i wykonać na niej testy mutacyjne 🙂

Po zakończeniu testów otrzymamy wygenerowany raport.

Możemy z niego wyczytać, że nasz kod jest w pełni pokryty przez nasze testy jednostkowe (Line Coverage). Możemy również zobaczyć trochę czerwonego koloru przy pokryciu mutacyjnych testów, a jak możemy się domyślać czerwony kolor nie oznacza nic dobrego 😉

Po wklikaniu się trochę głębiej będziemy mogli zobaczyć poniższy ekran.

Możemy na nim zobaczyć, która linia naszego programu nie jest wystarczająco dobrze przetestowana, a poniżej listę mutacji, które zostały przeprowadzone w poszczególnych liniach kodu. Na zielono są zaznaczone mutacje, które zostały wykryte przez testy, natomiast na czerwono mutacje, które przeżyły i nasze testy ich nie wychwyciły.

W naszym przypadku nie została wychwycona zmiana warunku w if’ie z > na >=. Czyli w tym przypadku został wykryty warunek brzegowy, który nie został sprawdzony w testach.

Poprawy w takim razie nasz drugi test tak, aby pokrył warunek brzegowy.

Po tej modyfikacji żadne mutacje nam nie straszne i nasze testy mutacyjne przejdą na zielono 🙂

Podsumowanie

Dzisiaj zapoznaliśmy się z podstawami testów mutacyjnych. Testy te mogą nam pomóc w sprawdzeniu jak dobre są nasze testy jednostkowe. Sama koncepcja testów mutacyjnych nie jest niczym nowym, ale dopiero stosunkowo od niedawna jest używana w praktyce, ponieważ testy mutacyjne są dosyć kosztowne i wymagają sporej czasu procesora, żeby przeprowadzić wszystkie kombinacje mutacji i dopiero od niedawna nasze komputery są na tyle szybkie, żeby robić to w rozsądnym czasie 🙂

XStream, czyli taka lepsza serializacja!

Dzisiaj omówimy bibliotekę służącą do serializacji obiektów w Javie – XStream.

Serializacja w Javie

Po co nam biblioteka do serializacji skoro sama Java dostarcza nam taki mechanizm? Spójrzmy zatem jak to wygląda w Javie.

Na początku stwórzmy sobie prostą klasę z kilkoma polami, której instancje będziemy serializować.

Teraz stwórzmy instancje naszej klasy i ją zserializujmy, a następnie zdeserializujmy.

Jak widzimy sama serializacja jest dosyć prosta i możemy ją łatwo wykonać w kilku liniach kodu.

Zerknijmy teraz do naszego pliku, który zawiera zserializowany obiekt.

Jak widzimy nie jest to zbyt czytelne 😉

Niestety z tego pliku nie jesteśmy w stanie w łatwy sposób odczytać zserializowanych wartości, nie mówiąc o ich edycji.

XStream w akcji!

I tutaj z pomocą przychodzi XStream!

Na początku musimy dodać zależność do naszego projektu:

Zróbmy teraz najprostszą serializacje i deserializacje naszego obiektu.

Jak widać operacje te przy użyciu XStream również możemy wykonać przy użyciu kilku linii kodu.

Na samym początku musimy stworzyć instancje XStream, a następnie wywołać na niej odpowiednio metody toXML lub fromXML. Pierwsza metoda przyjmuje jako parametry obiekt, który ma być zserializowany oraz OutputStream, do którego zserializowany obiekt ma być zapisany. Druga metoda natomiast na wejście przyjmuje InputStream, z którego ma być odczytany zserializowany obiekt, a na wyjściu otrzymujemy nasz zdeserializowany obiekt.

Zajrzyjmy teraz do pliku z zserializowanym obiektem.

Jak widać plik jest w formacie XML (co mogła sugerować nazwa metody toXML ;)). Z pliku możemy odczytać, która klasa jest zserializowana – pl.lantkowiak.SerializableClass. Widzimy też pełną strukturę klasy wraz z wartościami poszczególnych pól. Dzięki temu bez problemu możemy odczytać jak wygląda zapisany obiekt, a w razie potrzeby możemy go bez problemy edytować.

Podsumowanie

Dzisiaj zapoznaliśmy się z biblioteką XStream, której możemy użyć do serializacji obiektów do XMLa, dzięki czemu zserializowany obiekt będzie dla nas czytelny i łatwy do modyfikacji.

XStream posiada jeszcze bardziej zaawansowane opcje, z którymi warto się w przyszłości zapoznać 🙂

Działanie na kilka frontów – czyli o wątkach słów kilka

Dzisiaj porozmawiamy sobie o działaniu na kilka frontów 😉 Czyli zobaczymy co to jest wątek i z czym to się je 🙂

W tym wpisie zajmiemy się głównie sposobami tworzenia wątków.

Czym zatem jest ten wątek?

Wątek jest niezależną ścieżką wykonania działającą w ramach procesu.

Czym zatem jest proces? Proces jest to egzemplarz wykonywanego programu.

Jak zatem widzimy wątek jest wykonywany w ramach procesu. W ramach jednego procesu może być wykonywanych wiele wątków.

Powinniśmy również wiedzieć, że wątki dzielą zasoby pomiędzy sobą w ramach jednego procesu, natomiast każdy proces ma przydzielone swoje zasoby.

Wątki w Javie

W Javie wątki są reprezentowane przez klasę Thread.

Nowy wątek możemy utworzyć w Javie na 3 sposoby:

  1. Rozszerzając klasę Thread
  2. Implementując interfejs Runnable
  3. Implementując interfejs Callable

Czym się różnią te podejścia?

Rozszerzenie klasy Thread

Aby utworzyć wątek musimy rozszerzyć klasę Thread oraz nadpisać metodę run.

W metodzie run umieszczamy nasz kod, który zostanie wykonany w naszym nowym wątku.

Implementacja interfejsu Runnable

Interfejs Runnable wymaga przez nas zaimplementowania jednej  metody – run.

Podobnie jak w poprzednim przypadku kod, który ma być wykonany w wątku umieszczamy w metodzie run.

Implementacja interfejsu Callable

Interfejs Callable jest generyczny i dostarcza metodę call, która zwraca obiekt typu, którego użyliśmy przy implementacji Callable.

Tym razem kod, który ma się wykonać w nowym wątku umieszczamy w metodzie call. Dodatkowo call wymuszą zwrócenie obiektu. Zwróćmy również uwagę, że Callable deklaruje możliwość rzucenia wyjątku kontrolowanego (checked exception).

Co wybrać?

Poznaliśmy 3 sposoby tworzenia wątków w Javie, ale który powinniśmy używać?

Sposobu pierwszego, czyli rozszerzania klasy Thread będziemy prawdopodobnie używać najrzadziej. Musimy pamiętać, że dziedziczenie powinno spełniać warunek IS-A, czyli klasa pochodna powinna być typem klasy nadrzędnej. Raczej rzadko będziemy spotykać się z sytuacją gdzie tworzona przez nas klasa będzie musiała rozszerzać Thread, aby rozszerzać jej funkcjonalność. Więcej o dziedziczeniu i kompozycji porozmawiamy w jednym z kolejnych wpisów.

Czyli pozostały nam dwie opcje Runnable i Callable. Tutaj powinniśmy dokonać wyboru mając na uwadze różnice pomiędzy tymi interfejsami:

  • metoda run z interfejsu Runnable jest typu void, natomiast metoda call z Callable zwraca wynik obliczeń
  • metoda call pozwala zadeklarować wyjątki kontrolowane (checked exceptions), czego nie pozwala metoda run.

Podsumowanie

Dzisiaj omówiliśmy sposoby tworzenia wątków w Javie. Powinniśmy zapamiętać, że wątek możemy stworzyć na 3 sposoby:

  • rozszerzając klasę Thread,
  • implementując interfejs Runnable,
  • implementując interfejs Callable.

W następnym wpisie związanym z wątkami omówimy sposoby na uruchomienie i zarządzanie naszymi wątkami.

Niezmienne obiekty – po co nam one?

O ile pojęcie niezmiennych obiektów nie brzmi zbyt znajomo, to o tyle na pewno słyszałeś o Immutable Object. I o tym właśnie będzie ten wpis.

Czym jest Immutable Object?

Tutaj nie ma żadnego haczyka i nazwa dokładnie wskazuje o czym mówimy. Immutable object jest to niezmienny obiekt, czyli taki, którego stan nie może zostać zmieniony cały okres życia obiektu. Czyli po prostu tworzymy nasz obiekt wraz ze wszystkimi wymaganymi atrybutami i żadnego z nich nie możemy zmienić. Przynajmniej w teorii :)… ale zmiany poprzez refleksje się nie liczą, więc uznajemy, że nie mamy możliwości zmiany wartości tych atrybutów 😉

Jakie są zalety i wady tego podejścia?

Niezmienne obiekty, jak wszystko ;), mają zalety i wady. Do najważniejszych zalet według mnie należą:

  • Są łatwiejsze w użyciu i testowaniu
  • Można je bezpiecznie używać w Setach lub jako klucz w Mapach
  • Mogą być łatwo cachowane
  • Immutable object mogą być bezpiecznie używane w programowaniu wielowątkowym. Stan tych obiektów nie może ulec zmianie, więc mamy pewność, że każdy wątek widzi aktualny stan obiektu 😉

Wady Immutable Object:

  • Nadmiarowy kod – musimy dopisać kilka finali, ale za to pozbywamy się setterów 😉
  • Inicjalizacja wszystkich pól przez konstruktory. Ponieważ wszystkie nasze pola są oznaczone jako final, muszą więc zostać zainicjalizowane w konstruktorze. A co jak nie chcemy zawsze podawać wartości wszystkich parametrów, tylko użyć domyślnych wartości dla niektórych pól? Wtedy musimy stworzyć osobny konstruktor dla każdej kombinacji pól, którą chcemy użyć.
  • Problem z wydajnością – za każdym razem, gdy chcemy wprowadzić zmianę w naszym obiekcie to sprowadza się to do utworzenia nowego obiektu. Może to być odczuwalne zarówno w czasie działania aplikacji, jak i zużyciu pamięci.

Kiedy ich używać?

Jest na pewno kilka podstawowych use-caseów, kiedy powinniśmy rozważyć użycie niezmiennych obiektów:

  • programowanie wielowątkowe – jeżeli mamy obiekt, który ma być współdzielony pomiędzy wątkami to zdecydowanie warto rozważyć użycie immutable object
  • obiekt używany jako klucz (np. w mapach) – mamy wtedy pewność, że klucz nie zostanie zmieniony kiedy jest już w użyciu i nie będzie kolizji
  • obiekt ma być typowym ‚value object’ – wydaje się oczywiste 😉

Z drugiej strony powinniśmy skłaniać się ku ‚standardowym obiektom’, kiedy mamy do czynienia:

  • z dużymi obiektami, których tworzenie zajmie dużo czasu i/lub pamięci
  • obiektami, które posiadają ‚tożsaność’, tzn. reprezentują osoby/rzeczy, dla których zmiana pewnych parametrów jest naturalna np. samochód, dla którego naturalnymi jest zmiana takich parametrów jak prędkość czy poziom paliwa.

Implementacja Immutable Object w Javie

Stworzenie Immutable Object w Javie jest dosyć proste. Wystarczy przestrzegać kilku wskazówek 🙂

  1. Wszystkie pola powinny posiadać modyfikatory private i final.
  2. Nie tworzymy setterów (wynika to zresztą z użycia modyfikatora final przy polach).
  3. Musimy zabezpieczyć naszą klasę, żeby nie można było po niej dziedziczyć.
  4. Jeżeli pola naszej klasy zawierają mutable object, wtedy musimy zabezpieczyć te obiekty przed zmianą.

Pora na przykład, który zobrazuje nam powyższe zasady.

Trochę optymalizacji

Jeżeli nasz niezmienny object będzie na prawdę często używany w naszej aplikacji możemy rozważyć pewne optymalizacje.

Jedną z technik, którą możemy użyć jest Pooling. Jest on np. użyty w JVM w przypadku Stringów. Poniżej przedstawię bardzo prymitywną implementacje tego podejścia.

Na początku stwórzmy jeszcze nasz immutable object, którego użycie będizemy chcieli zoptymalizować.

I teraz prosty mechanizm poolingu:

Ogólnie mówiąc ideą jest powtórne użycie wcześniej utworzonego obiektu, jeżeli jest taki sam jak byśmy chcieli stworzyć.

Przy używaniu poolingu powinniśmy mieć na uwadze, że o ile w środowisku jednowątkowym powinien sprawdzić się całkiem nieźle, o tyle przy wielu wątkach narzut na synchronizacje może być znaczący.

Podsumowanie

We wpisie postarałem przedstawić się ideę niezmiennych obiektów, ich wady oraz zalety. Pokazałem również jak zaimplementować taki obiekt w Javie oraz zaproponowałem pewną optymalizacje, którą możemy użyć, aby zwiększyć wydajność naszej aplikacji w przypadku częstego korzystania z naszego immutable object.

UPDATE 14.05.2017

Kolega zwrócił mi uwagę na błąd, który był w listingu z implementacją niezmiennej klasy w Javie – teraz wszystko powinno być ok 🙂

Zwrócił również uwagę, że warto dodać wzmiankę o poolingu w środowisku wielowątkowym – dodane 🙂

Dzięki 🙂

Ile on waży?!

Czy zastanawialiście się kiedyś jak dużo pamięci zajmują obiekty w Javie? Dużo osób pewnie nigdy się nad tym nie głowiło, a przynajmniej nie przed pierwszym OutOfMemoryError 😉

W tym wpisie postaram się pokazać w jaki sposób oszacować wielkość obiektów, które tworzymy w Javie.

Typy prymitywne

Niewątpliwie najłatwiej jest określić ilość pamięci zajmowanej przez typy prymitywne. Poniżej przedstawiam tabelkę, która pokazuje ile każdy z typów potrzebuję bajtów w pamięci.

Typbajty
boolean1
byte1
char2
int4
float4
long8
double8

Ilość pamięci potrzebnej do przechowywania booleana może trochę dziwić. W końcu może on przyjąć tylko dwa wartości i w teorii powinien zajmować 1 bit. Specyfikacja JVM nie definiuje ile pamięci jest potrzebne na przechowanie booleana i zależy to od danej implementacji. Dla uproszczenia możemy śmiało przyjąć, że jest potrzebny 1 bajt.

Reszta wartości wydaję się być w miarę oczywista 🙂

Tablice

Każda tablica zajmuje wielokrotność danego typu (co wydaję się oczywiste ;)) oraz dodatkowe 24 bajty, które są narzutem tablicy. Poniżej przedstawiam tabelkę z przykładami, które powinny wszystko wyjaśnić.

Typbajty
byte[]N + 24
char[]2N+24
int[]4N+24
double[]8N+24

Obiekty

I teraz przechodzimy do mięsa tematu  – czyli jak oszacować wielkość obiektu naszej klasy.

Aby to zrobić potrzebujemy kilku dodatkowych informacji.

Narzut obiektu

Każdy obiekt, podobnie jak tablica, powoduje dodatkowy narzut pamięci. W tym przypadku jest to 16 bajtów.

Referencja

Każda przetrzymywana referencja w naszym obiekcie to dodatkowe 8 bajtów.

Padding

Wielkość każdego obiektu jest uzupełniana do wielokrotności 8 bajtów.

Praktyka 🙂

Jako przykład weźmy klasę String. Poniżej przedstawiam wycinek implementacji tej klasy, który jest potrzebny do oszacowania wielkości obiektu.

Do obliczenia potrzebujemy zsumować kilka rzeczy:

  1. Narzut obiektu – 16 bajtów
  2. Referencja do tablicy – 8 bajtów
  3. Tablica – 2N + narzut tablicy 24 bajty
  4. Pole offset – 4 bajty
  5. Pole count – 4 bajty
  6. Pole hash – 4 bajty

Zsumowując wszystko wychodzi nam 2N + 60 bajtów. W takim przypadku musimy jeszcze pamiętać o paddingu, czyli dodajemy 4 bajty, aby całkowita wielkość obiektu była wielokrotnością 8.

Podsumowanie

Powyżej przedstawiłem ogólne zasady, których możemy użyć, aby oszacować wielkość naszych obiektów w Javie. Dzięki temu możemy lepiej zrozumieć co jest alokowane w pamięci i jaki rozmiar zajmuje.

Adnotacje – starcie drugie!

W poprzednim wpisie o adnotacjach opisałem jak tworzyć adnotacje w Javie. I co dalej? Mamy już naszą adnotacje, więc wypadałoby ją jakoś użyć. W tym wpisie pokaże w jaki sposób możemy użyć naszych adnotacji.

Małe uzupełnienie poprzedniego wpisu

W poprzednim wpisie pominąłem jedną (przynajmniej :P) istotną sprawę przy tworzeniu adnotacji.

Dziedziczenie adnotacji

Przy tworzeniu adnotacji możemy użyć również @Inherited, który powoduje, że adnotacja będzie widoczną również jako obecna na klasach, które dziedziczą po klasie z tą adnotacją.

Zdefiniujmy naszą adnotacje następująco:

Klasę, która będzie oznaczona naszą adnotacją:

I naszą klasę dziedziczącą po MySuperClass:

W ten sposób MyAnnotation będzię widoczne również na MyClass.

Wracając do tematu…

Wydaję mi się, że najpopularniejszym przypadkiem użycia adnotacji są różnego rodzaju frameworki jak np. Hibernate czy Spring.

Jeżeli mamy naszą klasę oznaczoną np. adnotacją @Entity, to jak framework ‚znajduje’ informacje, że dana klasa ma właśnie tą adnotacje?

Jako przykład weźmy prostą adnotacje:

oraz klasę:

I teraz spróbujemy dobrać się do naszej adnotacji 🙂

Zacznijmy może od początku – czyli warto byłoby najpierw znaleźć klasy z adnotacjami, żebyśmy wiedzieli z czym będziemy pracować 😉

W Springu definiujemy pakiety, które mają być skanowane pod kątem adnotacji. Wyjdziemy z tego samego założenia – nie ma sensu skanować wszystkich klas, jeżeli nie jest to konieczne 🙂

Stwórzmy zatem dwie metody, dzięki którym będziemy mogli przejść rekurencyjnie po wszystkich folderach z danego pakietu o zwrócić listę wszystkich klas, adnotacji i interfejsów.

Metody są okraszone komentarzami, więc nie powinno być problemu ze zrozumieniem co tam się dzieje.

Metody nie robią co prawda nic skomplikowanego, ale po co pisać coś, co zostało już raz dobrze napisane 😉 Metody te pokazałem, żeby przedstawić jak to wygląda od podstaw, w najprostszej formie. W życiu codziennym polecam używanie odpowiednich metod z Spring, Guavy czy np. Reflections Library.

I teraz samo znajdowanie klas z naszą adnotacją:

Metoda wypisze na ekranie następującą linie:

Jak widać udało nam się znaleźć wszystkie (całą jedną w tym konkretnym przypadku ;)) klasy z adnotacją @MyAnnotation i odczytać wartości atrybutów adnotacji.

Podsumowanie

We wpisie został pokazany sposób w jaki można znaleźć klasy z konkretnego pakietu z konkretną adnotacją. Dzięki temu możemy zdefiniować własne zachowania/akcje dla klas z danymi adnotacjami.

Oczywiście wpis ten nie wyczerpuje tematu. Mechanizm refleksji dostarcza wielu innych metod związanych ze znajdywaniem klas czy adnotacji. Zachęcam do zagłębienia się w ten temat 🙂

Adnotacje w Javie

W Javie 1.5 zostały wprowadzone adnotacje. Możemy je często spotkać przy pisaniu testów jednostkowych, ORMach lub różnych frameworkach. Czym są właściwie adnotacje i co nam dają?

Adnotacje są to metadate, które dostarczają nam informacji na temat programu, ale same nie są częścią kodu, ani bezpośrednio nie wpływają na kod.

Najprostszą adnotacje możemy stworzyć w następujący sposób:

Teraz możemy użyć jej w naszym kodzie:

Pola

Do naszych adnotacji możemy dodawać pola. Aby to zrobić musimy je zdefiniować w ciele naszej adnotacji.

I samo użycie:

Jeżeli nasze pole będzie miał nazwę value, wtedy możemy pominąć nazwę pola w użyciu adnotacji.

Domyślne wartości pól

W przykładach powyżej podanie wartości zdefiniowanych pól jest obowiązkowe. Jeżeli chcemy, aby podanie pola nie było wymagane musimy zdefiniować jego domyślną wartość.

Wtedy przy użyciu adnotacji możemy, ale nie musimy, podawać wartość pola.

Zakres adnotacji

Adnotacje domyślnie mogą być używane na:

  • innych adnotacjach
  • konstruktorach
  • metodach
  • polach
  • lokalnych zmiennych
  • pakietach
  • parametrach metod
  • klasach, interfejsach, enumach
  • typach generycznych (od Java 8)

Domyślnie nowo stworzona adnotacja może być używana w każdym z  wymienionych wcześniej miejsc. Możemy jednak ograniczyć jej zasięg poprzez użycie adnotacji @Target z parametrami definiującymi zasięg.

Wszystkie dostępne wartości ElementType to:

  • ANNOTATION_TYPE
  • CONSTRUCTOR
  • METHOD
  • FIELD
  • LOCAL_VARIABLE
  • PACKAGE
  • PARAMETER
  • TYPE
  • TYPE_PARAMETER

Kolejne wymienione wartości ElementType odpowiadają wcześniej wymienionym miejscom opisującym gdzie można użyć adnotacji.

Dodatkowo w Javie 8 wprowadzono ElementType.TYPE_USE, który może być użyty na wszystkich typach.

Retencja adnotacji

Dla każdej adnotacji możemy ustalić jej zakres widoczności. Definiujemy to przez dodanie adnotacji @Retention.

Dostępne są trzy wartości dla adnotacji @Retention:

  • RetentionPolicy.SOURCE – informacja o adnotacji jest dostępna jedynie podczas kompilacji; przykładami takich adnotacji są @Override i @SuppressWarnings
  • RetentionPolicy.CLASS – adnotacja jest dostępna podczas ładowania klas; jest to domyślny poziom retencji
  • RetentionPolicy.RUNTIME – adnotacja jest dostępna podczas działania aplikacji, dzięki czemu mamy do niej dostęp poprzez refleksje; przykładem takiej adnotacji jest @Deprecated

Krótkie podsumowanie

W wpisie przedstawiłem najważniejsze informacje na temat adnotacji w Javie. Mam nadzieję, że okażą się one przydatne i choć trochę ułatwią poruszanie się po frameworkach, które pełne są najróżniejszych adnotacji 🙂

Kontrakt hashCode i equals

Jedną z pierwszych rzeczy, o których się dowiadujemy ucząc się Javy są informacje na temat metod equals oraz hashCode. Informacje na temat kontraktu tych dwóch metod są bardzo istotne w programowaniu w Javie, szczególnie w przypadku kolekcji, ale o tym będzie kiedy indziej.

Kontrakt

Metody equals oraz hashCode powinny spełniać poniższe warunki:

  • Jeżeli x == y to x.equals(y) == true
  • Jeżeli x.equals(y) == true to x.hashCode() == y.hashCode()
  • Jeżeli x.hashCode() == y.hashCode() to x.equals(y) może zarówno zwrócić true, jak i może zwrócić false

Wszystkie wartości zwracane przez powyższe funkcje muszą być determistyczne – zawsze zwracać te same wartości dla tych samym parametrów.

Domyślna implementacja

Metody hashCode i equals są zdefiniowane w Object, czyli w klasie nadrzędnej dla wszystkich klas w Java. Domyślne implementacje wyglądają następująco:

Domyślna implementacja metody equals opiera się na sprawdzeniu czy przekazany obiekt jest tą samą instancją, co obiekt, na którym wywołano metodę equals. Jak widać domyślna implementacja w żaden sposób nie opiera się na atrybutach klasy.

Klasa Object nie ma natomiast zdefiniowanego ciała metody hashCode. Każda implementacja JVM dostarcza swoją wersje tej metody. Zazwyczaj jest to adres pamięci, pod którym znajduje się obiekt, zamieniony na typ integer. Podejście takie byłoby prawidłowe, gdyby każdy obiekt był unikalny. W przypadku kiedy dwa obiekty są identyczne (bazując na atrybutach klasy) implementacja taka nie spełnia kontraktu tej metody.

Przykładowe implementacje

Oczywiście powyższe metody możemy (a często wręcz musimy) nadpisać i dostosować do naszych potrzeb. Załóżmy, że mamy poniższą klasę, do której chcemy dopisać nasze dwie metody, o których rozmawiamy.

Przykładowe implementacja metody hashCode mogą wyglądać następująco:

Jak widać wartość hashCode jest wyliczana przy użyciu mnożenia i dodawanie na podstawie wszystkich pól klasy.

Implementacja equals dla naszej klasy może wyglądać tak:

Tutaj już się dzieję trochę więcej… albo przynajmniej wygląda jakby się działo więcej.
W liniach 3-11 sprawdzamy identyczność obiektów na poziomie samego obiektu, a nie jego atrybutów. Sprawdzamy czy:

  • przekazany obiekt jest tym samym obiektem, na którym została wywołana metoda – jeżeli jest to oczywiście zwracamy true
  • przekazany obiekt nie jest nullem – jeżeli jest to zwracamy false – obiekt, na ktorym wywołaliśmy metodę z oczywistych powodów nie może być nullem 🙂
  • oba obiekty są tego samego typu – jeżeli nie są to oczywiście zwracamy false

Jeżeli obiekt przejdzie te 3 warunki to jest rzutowany na nasz typ klasy, a następnie każde pole klasy jest porównywane. Jeżeli, któreś z porównywanych pól jest inne to oczywiście zwracamy false, a w przeciwnym przypadku przechodzimy dalej. Kiedy już wszystkie pola zostały sprawdzony to zwracamy true.

Bardziej praktyczna implementacja

Implementacje powyższych metod nie są trudne, ale zajmują trochę linii kodu (szczególnie equals). Dodatkowo implementując ręcznie te metody dla klas z większą ilością pół łatwo o pomyłke. Co prawda każde IDE potrafi wygenerować implementacje powyższych metod na podstawie pól klasy, ale wciąż zostaje problem pierwszy – dużo linii i brzydko to wygląda 😉

Bardzo polecam używanie bibliotek, które zawierają wbudowany builder’y do tych metod. Jedną z takich bibliotek jest common-lang od Apache.
Implementacja hashCode przy użyciu tej metody wygląda następująco:

Jak widać kod dla tej metody został zdecydowanie uproszczony i jest bardziej przejrzysty.

Implementacja metody equals może wyglądać na przykład tak:

Jak widać kod w tym przypadku również się trochę uprościł. Trzy pierwsze if’y co prawda zostały, ale reszta kodu została zdecydowanie skrócona i jest bardziej czytelna.

Jak widać dzięki użyciu kodu z biblioteki mogliśmy stworzyć nasze metody pisząc mniej kodu, który jest jednak bardziej czytelny i przejrzysty.

A może by tak jeszcze coś ulepszyć?

Patrząc na implementacje metody equals można by się zastanowić czy użycie instanceof zamiast drugiego i trzeciego if’a nie byłoby dobrym pomysłem. Wtedy nasza klasa mogłaby wyglądać w następujący sposób:

Dzięki takiemu podejściu ‚oszczędzamy’ jednego if’a, a wszystko działa jak działało. Chociaż…

Spójrzmy na poniższą klase:

Stworzyliśmy klasę Student, która dziedziczy po Person. Nasza nowa klasa zawiera dodatkowe pole ‚school’.

Zróbmy teraz coś takiego:

Jak widać dochodzimy do sytuacji, gdzie tworzymy dwa różne obiekty i w zależności od tego, który do którego porównamy to otrzymujemy różne wyniki. Równość powinna być relacją symetryczną, a taka sytuacja zdecydowanie ją narusza.

Czy to oznacza, że to podejście jest błędne? Odpowiedź jest bardzo prosta i brzmi: to zależy 🙂

Wersja equals z getClass jest, powiedzmy, ‚restrykcyjna’, ale za to prosta i intuicyjna. Porównywany obiekt musi być dokładnie tego samego typu co obiekt, do którego jest porównywany.

Druga wersje może narobić nam trochę ambarasu, lecz zostawia trochę więcej swobody. Istotnym przypadkiem, kiedy takie podejście może być konieczne jest porównywanie obiektów, które są zarządzane przez jakiś framework np. część implementacji JPA opakowuje encje. W takiej sytuacji, dzięki użyciu instanceof w metodzie equals jesteśmy w stanie w poprawny sposób porównywać takie obiekty.

Jak widać obydwa sposoby implementacji metody equals mają swoje zastosowania. Najważniejsze, żeby świadomie wybierać podejście, którego potrzebujemy 🙂