
Null pointer exception jest chyba najbardziej rozpoznawalnym i najczęściej występującym wyjątkiem w Javie. Wyjątek ten może doprowadzać do wielu niepożądanych zachowań (w tym przerwanie działania aplikacji). Java 8 dostarcza nam nową klasę Optional z pakietu java.util.*, która pozwala nam w lepszy sposób zabezpieczyć się przed tego typu wyjątkiem.
Problem
Na początku przyjrzyjmy się jak to było po staremu. Nasza metoda odczytuje dane z bazy danych. W przypadku gdy dane nie wystąpią możemy to obsłużyć na kilka sposobów:
Product getValueBy(final String id) {
return readFromDB(id);
}
Product getValueBy(final String id) {
Product product = readFromDB(id);
if (product == null) {
throw new IllegalStateException();
}
return product;
}
Lub spróbować opakować wynik w nasz obiekt (przykładowo DataBaseResult, który przechowuje wartość jeśli istnieje). Od Javy 8 sami już nie musimy tworzyć klasy opakowującej ponieważ, otrzymaliśmy klasę Optional.
Tworzenie
Klasę Optional można stworzyć na kilka sposobów. Sposób tworzenia zależy od efektu jaki chcemy osiągnąć.
Metoda of
Metoda of służy do opakowania wartości jako typ Optional. Z metodą of należy uważać, jeśli wrzucimy do środka wartość null ponieważ, dostaniemy wyjątek typu NullPointerException:
@Test
void shouldThrowNullPointerExceptionWhenValueIsNullOnOf() {
// When
Executable optionalWithNull =
() -> Optional.of(null);
// Then
assertThrows(NullPointerException.class, optionalWithNull);
}
@Test
void shouldReturnTrueWhenValueIsPresent() {
// Given
Optional
// When
boolean isPresent = optional.isPresent();
// Then
assertThat(isPresent).isTrue();
}
Metoda ofNullable
Metoda ofNullable w przeciwieństwie do metody of służy do opakowania wartości, która może być null'owa. Tym razem jeśli wrzucimy wartość null to nie dostaniemy wyjątku:
@Test
void shouldNotThrowNullPointerExceptionWhenValueIsNullOnOfNullable() {
// When
Executable optionalWithNull =
() -> Optional.ofNullable(null);
// Then
assertAll(optionalWithNull);
}
Metoda empty
Ostatnim sposobem stworzenia wartości Optional jest metoda empty. Tworzy ona pustego Optional'a. Może być on wykorzystywany jako wartość domyślna zwracana z metody:
Optional
if (StringUtils.isBlank(id)) {
return Optional.empty();
}
return Optional.ofNullable(readFromDB(id));
}
Sprawdź zanim pobierzesz
Wiemy już w jaki sposób opakować wartość jako opcjonalna. Teraz chcielibyśmy tą wartość odczytać. Jednakże, w przypadku Optional zawsze powinniśmy sprawdzić czy wartość istnieje. Do tego wykorzystujemy metodę isPresent:
@Test
void shouldReturnTrueWhenValueIsPresent() {
// Given
Optional
// When
boolean isPresent = optional.isPresent();
// Then
assertThat(isPresent).isTrue();
}
@Test
void shouldReturnFalseWhenValueIsNotPresent() {
// Given
Optional
// When
boolean isPresent = optional.isPresent();
// Then
assertThat(isPresent).isFalse();
}
Ponadto otrzymaliśmy bardziej funkcyjną metodę do wywołania logiki w przypadku wystąpienia danych. Jest to metoda ifPresent, która przyjmuje Consumer:
@Test
void shouldCallMethodWhenValueIsPresentOnIfPresent() {
// Given
Optional
Slepper slepper = Mockito.mock(Slepper.class);
doCallRealMethod().when(slepper).consumer(“call”);
// When
optional.ifPresent(slepper::consumer);
// Then
verify(slepper).consumer("call");
}
Transformacje
Na samej wartości Optional możemy składać dodatkowe transformacje takie jak:
filter- filtruje danemap- zmienia typflatMap- “spłaszczenie” danych
String getProductValueBy(final String id, final String productName) {
return getValueBy(id)
.filter(product -> product.productName.equals(productName))
.flatMap(product -> getValueBy(product.productName))
.map(product -> product.productName)
.map(String::toUpperCase)
.orElseGet(this::getDefaultValue);
}
W przeciwnym wypadku
Pobranie wartości, która jest opcjonalna pozwala nam definiować domyślne wartości lub zachowaniu w przypadku gdy Optional jest pusty.
orElse
Jeśli chcemy zwrócić wartość domyślną w przypadku gdy Optional jest pusty to możemy do tego celu wykorzystać metodę orElse.
@Test
void shouldReturnDefaultWhenValueIsNotPresentOnOrElse() {
// Given
Optional
// When
String value = optional.orElse("orElse");
// Then
assertThat(value).isEqualTo("orElse");
}
Jednakże z metodą orElse należy uważać. Jeśli zamiast zwracania wartości wywołamy metodę, to metoda ta zostanie wywołana nawet wtedy kiedy wartość w Optional istnieje:
@Test
void shouldCallMethodWhenValueIsPresentOnOrElse() {
// Given
Optional
Slepper slepper = Mockito.mock(Slepper.class);
when(slepper.getDefaultValue()).thenCallRealMethod();
// When
String value = optional.orElse(slepper.getDefaultValue());
// Then
verify(slepper).sleep();
assertThat(value).isEqualTo("orElse");
}
class Slepper {
String getDefaultValue() {
sleep();
return "default";
}
void sleep() {
}
}
orElseGet
Rozwiązaniem nadgorliwej metody orElse jest leniwa metoda orElseGet. Jako parametr przyjmuje ona Suppiler:
@Test
void shouldReturnDefaultWhenValueIsNotPresentOnOrElseGet() {
// Given
Optional
// When
String value = optional.orElseGet(() -> "orElse");
// Then
assertThat(value).isEqualTo("orElse");
}
@Test
void shouldNotCallMethodWhenValueIsPresentOnOrElseGet() {
// Given
Optional
Slepper slepper = Mockito.mock(Slepper.class);
when(slepper.getDefaultValue()).thenCallRealMethod();
// When
String value = optional.orElseGet(slepper::getDefaultValue);
// Then
verifyZeroInteractions(slepper);
assertThat(value).isEqualTo("orElse");
}
orElseThrow
Czasami zakładamy, iż dane o które odpytujemy bazę danych muszą wystąpić. Jeśli natomiast nie wystąpią oznacza to dla nas sytuację wyjątkową. Sytuacje wyjątkowe najczęściej obsługujemy poprzez wyjątki. Jeśli w przypadku braku wartości chcemy rzucić wyjątek, to możemy zastosować metodę orElseThrow (podobnie jak orElseGet też jest leniwa):
@Test
void shouldThrowIllegalStateExceptionWhenValueIsEmpty() {
// Given
Optional
// When
Executable optionalWhichThrowException =
() -> optional.orElseThrow(() -> new IllegalStateException());
// Then
assertThrows(IllegalStateException.class, optionalWhichThrowException);
}
Optional jako parametr metody
Optional powinien być wykorzystywany tylko do zwracania opcjonalnych danych. Nie powinniśmy stosować klasy Optional jako typ w paramaterze metody. Jeśli posiadamy metodę, w której jeden z parametrów jest opcjonalny to możemy taką metodę przeciążyć (OOP):
void methodWithOptional(
final String value,
final Optional
// some logic here
}
void methodOverload1(final String value) {
// some logic here
}
void methodOverload2(
final String value,
final String secondValue) {
// some logic here
}
Optional a serializacja
Zdefiniowanie pola typu Optional może powodować problemy jeśli nasza klasa jest serializowana, ponieważ typ Optional nie jest serializowalny.
GitHub
Całość jak zawsze na GitHub’ie.