W poprzednim wpisie dotyczącym Guavy omówiliśmy klasę Preconditions, która mówiąc najprościej dostarcza zamienników dla często występujących idiomów, które służą do „twardego” sprawdzania parametrów metody. „Twarde” oznacza, że w przypadku niespełnienia warunku jest rzucany wyjątek.

Dziś przyjrzymy się sytuacji gdzie pomimo braku parametru, wartość null, możemy kontynuować pracę ponieważ potrafimy sobie poradzić z takim brakiem. Nie jest to sytuacja rzadka czy nietypowa. Bardzo często w przypadku braku parametru tworzymy kod w rodzaju:

Listing 1. Kod wykorzystujący wartości domyślne

public void method(Object p1){
    if(p1 == null)
       p1 = DEFAULT_VALUE;
    //...
}

Co więcej, miejsc w kodzie, gdzie powstają takie konstrukcje jest zazwyczaj od groma i ciut ciut. Szczególnie często występują one tam gdzie korzystamy ze wzorców „łańcuchowych” (Chain of Constructors/Methods) wołając coraz to kolejne wersje i przekazując im null. Co zabawne w ostatniej, najbardziej rozbudowanej wersji metody zazwyczaj następuje ifologia zastępująca null wartościami domyślnymi. W sumie i tak ta ifologia musi się tam znajdować zatem nie ma tu lepszego rozwiązania… no może poza przekazywaniem wartości domyślnych ze wcześniejszych wywołań i tym samym jakiejś tam pseudo optymalizacji omijającej w takim przypadku nieużywane IF-y… nieee…. fuuj…

Klasa Optional pozwala na pozbycie się tego jakże szkodliwego kodu jednocześnie dostarczając metodę (jedną biedaczkę), która w razie czego zachowa się jak Preconditions.chechNotNull i walnie błędem.
Samo działanie klasy, w tej fajniejszej wersji, przypomina trochę to jak działa Option ze Scali, ale jak to w czystej javie bywa jest z nią więcej pieprzenia.

Tworzenie instancji Optional

Na zdrowy chłopski rozum klasa ta powinna dawać możliwość stworzenia się w dwóch wersjach. Pierwszej, która powstanie gdy przekażemy obiekt, który nie jest null i w drugiej dla null. I generalnie to tak działa. Klasa Optional jest jako taka klasą abstrakcyjną i posiada dwie podklasy – Present dla istniejących obiektów i Absent, która jest singletonem reprezentującym wartości null.

Proces tworzenia obiektu może zatem przebiegać bardzo prosto:

Listing 2. Tworzymy obiekt Optional

public Integer forceNPE(Integer param) {          
	Optional<Integer> oParam = of(param);   
	return oParam.get();
}

Taka konstrukcja tworzy pod spodem obiekt klasy Present, która może zostać użyta w dalszym kodzie. Rzecz w tym, że uruchomienie kodu takiego jak ten:

Listing 3. Tworzymy obiekt Optional i mamy NPE

@Test(expectedExceptions = NullPointerException.class)
public void testForceNPEThrowNPE() {                  
	assertThat(oe.forceNPE(null)).isEqualTo(5);          
}

ujawnia pewien mankament. Jak wspomniałem wcześniej klasa posiada jedną metodę, która zachowuje się jak Prconditions.chechNotNull i jest to właśnie metoda tworząca of. Czyli cały nasz plan można wywalić, bo po co Optional skoro mamy Preconditions.
Nie załamujmy się jednak. Wbrew pozorom to nie metoda or jest najważniejsza. Znacznie ważniejsza jest metoda fromNullable. Zamieńmy nasz kod na następujący:

Listing 4. Tworzymy obiekt Optional drugie podejście

public Integer supressNPE(Integer param) {         
	Optional<Integer> oParam = fromNullable(param);   
	return oParam.or(DEFAULT_INT);                    
}

Jeżeli teraz odpalimy proste testy:

Listing 5. Tworzymy obiekt Optional i nie ma NPE

@Test                                                                       
public void testSupressNPE() throws Exception {                             
	assertThat(oe.supressNPE(5)).isEqualTo(5);                                 
	assertThat(oe.supressNPE(null)).isEqualTo(OptionalExamples.DEFAULT_INT);   
}

to naszym oczom ukaże się zielony pasek. Jest zatem całkiem nieźle. Użycie metody fromNullable powoduje, że biblioteka wybiera w zależności od parametru jaki dokładnie rodzaj Optional nam dostarczyć. Następnie wywołując metodę or może zajść jedna z dwóch ścieżek. Pierwsza to ta gdy oryginalny obiekt jest null, wtedy to po sprawdzeniu czy zastępnik też nie jest or zostaje on zwrócony. Druga to ta gdy oryginalny obiekt nie jest null. Wtedy metoda or zachowuje się jak inna metoda, get i zwraca oryginalny obiekt.

Na chwilę obecną klasa Optional nie przedstawia nam się zbyt zachęcająco. W sumie jej „wartość dodana” wynikająca z dołączenia jej do kodu jest ograniczona do eliminacji IF-ów. Przyjrzyjmy się jednak co więcej zawiera ta klasa, czyli czas na klasyczne omówienie API.

Metoda absent

Metoda statyczna zwracająca instancję klasy Absent. Nic ciekawego, ale zapewne znajdzie się dla niej zastosowanie wraz ze wzorcem null object.

Metoda asSet

Po drodze oryginalny obiekt jest opakowywany w niezmiennego Seta. Jeżeli przekażemy null to w efekcie otrzymamy pusty, niezmienny Set. Niby mało ciekawe, ale jeżeli zastanowić się nad tym dokładniej to w ten sposób otrzymujemy bardzo elegancki mechanizm do eliminacji efektów ubocznych w funkcjach, które stosujemy na zbiorach albo zwracających zbiory.

Metody equals, hashCode i toString

Metody te są zaimplementowane w intuicyjny sposób. Pierwsza z nich wywołuje metodę equals na referencjach do oryginalnych obiektów (o ile oczywiście trafi do niej Optional, a tak na prawdę Present). Druga wywołuje hashCode oryginalnego obiektu „domieszkując” go o swoje przesunięcie. Trzecia z metod też domieszkuje oryginalne wywołanie o informację, że jest to jednak Optional.

Metoda fromNullable

W zależności od przekazanego argumentu zwraca Present jeżeli nie jest on null albo Absent w przeciwnym wypadku.

Metoda get

Metoda ta zwraca referencję do obiektu. Jeżeli zostanie wywołana na obiekcie klasy Absent to rzuci IllegalStateException.

Metoda isPresent

Zwraca true…. a zresztą czy trzeba tłumaczyć?

Metoda of

Jeżeli jako argument przekażemy jej istniejący obiekt to otrzymamy Optional. Przekazując null otrzymamy NullPointerException. Metodę tą omówiłem już wcześniej. Generalnie może wprowadzić trochę zamieszania, bo zamiast eleganckiej obsługi null wali wyjątkiem.

Metoda or

Metoda ta występuje w trzech wersjach. Z pierwszą, przyjmującą domyślna wartość, już się spotkaliśmy. Druga wersja przyjmuje jako parametr inny obiekt Optional i ją zwraca. Trzecia wersja metody przyjmuje jako parametr Supplier i zwraca jego produkt.
Oczywiście dotyczy to tylko null. W przypadku normalnych obiektów zostanie zwrócony oryginał.

Metoda orNull

Metoda zwróci dokładnie to co jej przekażemy jako argument. Jak przekażemy null to dostaniemy null.

Metoda presentInstances

Parametrem tej metody jest coś po czym można się iterować. Z oryginalnego zbioru zostaną usunięte wszystkie wystąpienia obiektów Abasent. Zwrócony zbiór będzie zawierać tylko te wartości, które nie są wartościami domyślnymi.

Metoda transform

Jako argument przyjmuje funkcję, która o ile istnieje oryginalny obiekt, zostanie zaaplikowana i zwróci Optional przechowujący wynik jej działania. Jeżeli mamy do czynienia z null zostanie zwrócony Absent.

Podsumowanie

Przedstawione tu rozwiązanie nie jest najlepsze. Pozwala jednak na pójście „trzecią drogą” gdzieś pomiędzy wyrzucaniem wyjątku, a ręczną obsługą wartości null. Doskonale nadaje się też jako wsparcie w programowaniu funkcyjnym gdy trzeba jakoś eliminować niepożądane efekty uboczne wynikające z przekazywania do funkcji i predykatów wartości null.