www.arturstepkowski.pl
Optional - nigdy więcej NullPointerException
Użycie i dobre praktyki
www.arturstepkowski.pl
O czym będzie?
1. Jak sobie radziliśmy przed Java 1.8?
2. Jak używać kontenera Optional
3. Dobre praktyki stosowania Optional
www.arturstepkowski.pl
Jak sobie radziliśmy przed Java 1.8
www.arturstepkowski.pl
return null
public MyObject get(final long id) {
...
if(resultSet.next()) {
return prepareMyObject(rs);
}
return null;
}
www.arturstepkowski.pl
Chyba najgorsze z możliwych rozwiązań. To było proszenie
się o kłopoty. NullPointerException był praktycznie
oczywistością. O ile jeszcze twórca funkcji pobierającej dane
wiedział, że zwraca ona null i mógł się przed tym
zabezpieczyć odpowiednim if-em, to każdy inny programista
wcześniej czy później natrafiał na NPE.
www.arturstepkowski.pl
throw exception
public MyObject get(final long id)
throws MyObjectNotFoundException {
...
if(resultSet.next()) {
return prepareMyObject(rs);
}
throw new MyObjectNotFoundException();
}
www.arturstepkowski.pl
Rozwiązanie lepsze, bo nie ma już ryzyka NPE, ale
używanie wyjątków w sytuacjach możliwych i naturalnych
w aplikacji jest dyskusyjne, a dodatkowo w każdym
przypadku trzeba napisać bloki try-catch.
www.arturstepkowski.pl
Wzorzec NullObject
public abstract class AbstractMyObject {
protected String name;
public abstract String getName();
public abstract void setName(String name);
// reszta koniecznych metod
}
Stosując ten wzorzec w pierwszej kolejności musimy utworzyć klasę abstrakcyjną.
www.arturstepkowski.pl
public class MyObject extends AbstractMyObject {
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
}
Następnie tworzymy klasę główną, która rozszerza nam tę klasę abstrakcyjną...
www.arturstepkowski.pl
public class NullObject extends AbstractMyObject {
@Override
public String getName() {
return ""; // zwracamy wartość domyślną
}
@Override
public void setName(String name) {
// metoda pusta
}
}
oraz klasę “nullową”, która także rozszerza wspomnianą klasę abstrakcyjną.
www.arturstepkowski.pl
To rozwiązanie symulowało częściowo to, co dostaliśmy po wprowadzeniu
Optional<T>. Brak ryzyka NPE, brak niepotrzebnej obsługi wyjątków,
dodatkowo można w tym rozwiązaniu odwoływać się do metod interesującej nas
klasy.
Minusy?
● Dla każdego przypadku musimy tworzyć klasę abstrakcyjną, właściwą
klasę dla obiektów oraz wydmuszkę NullObject
● Problem z rozszerzalnością. Za każdym razem musimy edytować trzy
klasy
● Brak wiedzy o wartości zwracanej przez metody klasy NullObject
● wykorzystanie tego wzorca w przypadku takich klas jak String,
Integer, Boolean itp. komplikuje kod
www.arturstepkowski.pl
Jak używać kontenera Optional
www.arturstepkowski.pl
Odkąd dostępna jest klasa Optional nie musimy się już
martwić opisanymi przed chwilą problemami. Zwracana
wartość jest opakowywana w kontener, a zamiast null
zwracamy Optional.empty(). W tym momencie
otrzymujemy samodokumentujący się kod. Inny
programista (albo my sami po tygodniu) od razu będzie
widział w jaki sposób obsłużyć zwracaną wartość.
www.arturstepkowski.pl
public Optional<MyObject> get(final long id) {
// ...
if(resultSet.next()) {
return Optional.of(prepareMyObject(rs));
}
return Optional.empty();
}
Przykład opakowania zwracanego obiektu w Optional<T>
www.arturstepkowski.pl
Co dalej?
Jak używać obiektu Optional?
Jakie metody można na nim
wywoływać?
www.arturstepkowski.pl
isPresent() – Sprawdzenie czy obiekt ma ustawioną
wartość
Optional<MyObject> myObject = getMyObject();
if(myObject.isPresent()) {
// ...
}
Od Java 11 mamy do dyspozycji metodę isEmpty(), która, jak można się domyślić,
pozwala na sprawdzenie, czy kontener Optional jest pusty.
www.arturstepkowski.pl
get() – pobranie wartości z kontenera
Optional<MyObject> opt = getMyObject();
if(opt.isPresent()) {
MyObject myObject = opt.get();
// ...
}
Jeżeli Optional zawiera wartość, to jest ona zwracana. Użycie metody get() powinno
być poprzedzone sprawdzeniem czy Optional jest niepusty lub zastąpione użyciem
metody orElse() (patrz dalej).
www.arturstepkowski.pl
orElse(T other) – pobranie wartości domyślnej
Optional<String> opt = geUserFullName();
String userFullName = opt.orElse("");
Użycie metody orElse powoduje zwrócenie zawartości kontenera Optional jeżeli ta
jest ustawiona lub podanej wartości domyślnej, jeżeli kontener jest pusty. Wartość
domyślna powinna być przemyślana. Ustawienie jej na null w sporej części
przypadków nie będzie dobrym rozwiązaniem.
Jak można zauważyć użycie orElse skraca kod i jednocześnie czyni go czytelniejszym.
www.arturstepkowski.pl
filter() - filtrowanie, zamiennik if
Optional<MyObject> opt = getMyObject();
if(opt.isPresent()) {
MyObject myObject = opt.get();
if(myObject.getValue() == 10) {
return myObject.getName();
}
}
Zamiast:
www.arturstepkowski.pl
Optional<MyObject> opt = getMyObject().filter(o->o.getValue()== 10);
if(opt.isPresent()) {
return opt.get()
.getName();
}
lepiej jest napisać:
www.arturstepkowski.pl
Mapowanie
Optional<String> opt = getMyObject().map(o->o.getName());
if(opt.isPresent()) {
return opt.get();
}
Ta funkcjonalność pozwala nam na zmapowanie obiektu z kontenera w inny.
Najczęściej stosuje się to po to, żeby wydobyć wartość konkretnych pól.
www.arturstepkowski.pl
Optional<String> opt = getUser().flatMap(u->u.getNickname());
String nickname = opt.orElse("");
A co w przypadku, gdy funkcja użyta w map zwraca Optional? Dostaniemy
Optional<Optional<T>>. Niezbyt ciekawa perspektywa, ale na szczęście
mamy funkcję flatMap na takie sytuacje. Rozważmy klasę User, gdzie
metoda getNickname() zwraca Optional<String> (użytkownik może mieć
ksywkę, ale nie musi).
www.arturstepkowski.pl
Dobre praktyki
www.arturstepkowski.pl
Jako argumentu orElse nie używaj funkcji
Optional<MyObject> opt = getMyObject();
MyObject myObject = opt.orElseGet(()->getDefaultValue());
Metoda orElse ma taki drobny feler, że jeżeli jako parametru wywołania użyjemy
funkcji to zostanie ona zawsze wywołana, nawet jeśli Optional zawiera wartość.
Zwłaszcza w przypadku kiedy wywoływana funkcja jest kosztowna czasowo, to
znacznie lepiej jest wykorzystać metodę orElseGet().
www.arturstepkowski.pl
Nigdy nie przypisuj wartości null do zmiennej Optional
Optional<String> opt = null; // ŹLE
Optional<String> opt = Optional.empty(); // DOBRZE
Optional jest kontenerem i przypisywanie zmiennym tego typu wartości null jest
kompletnie bez sensu.
www.arturstepkowski.pl
Jeśli musisz wykonać działanie tylko wtedy kiedy
wartość w kontenerze jest ustawiona użyj ifPresent
// ŹLE
Optional<String> opt = getUser().map(u->u.getEmail());
if(opt.isPresent()) {
new EmailSender().send(opt.get());
}
// DOBRZE
Optional<String> opt = getUser().map(u->u.getEmail());
opt.ifPresent(e->new EmailSender().send(e));
Rozważmy sytuację, kiedy chcemy wysłać maila tylko wtedy kiedy user ma ustawiony
adres e-mail.
www.arturstepkowski.pl
Nie nadużywaj Optional w prostych sytuacjach
// NIEWŁAŚCIWIE
Long counter = count();
return Optional.ofNullable(counter).orElse(0L);
// ZNACZNIE LEPIEJ
Long counter = count();
return null == counter ? 0L : counter;
Jeżeli chcemy sprawdzić czy zmienna przenosząca wartość jest null i ustawić wtedy
wartość domyślną, nie powinniśmy w większości wypadków używać Optional.
Przykładowo:
www.arturstepkowski.pl
Nie deklaruj pól w klasie jako Optional
Zasada ta dotyczy zwłaszcza tych klas, które podlegają serializacji, gdyż klasa Optional
nie implementuje interfejsu Serializable. Ale uważa się, że zasadę tę należy stosować
do wszystkich klas, z dwóch powodów:
● Warto stosować jeden schemat tworzenia klas, niezależnie od tego czy są
serializowalne czy też nie.
● Stworzenie w klasie pola typu Optional<T> tak naprawdę nie daje nam żadnej
przewagi. Jeżeli klasa przenosi dane, to w większości wypadków będzie
serializowalna. Jeżeli odpowiada za logikę biznesową, to użycie w niej zmiennej
globalnej samo w sobie nie jest dobrą praktyką
www.arturstepkowski.pl
public class User {
private String nickname; // pole bez Optional
// ...
// getter zwraca Optional
public Optional<String> getNickname() {
return Optional.ofNullable(nickname);
}
// ...
}
Przykład:
www.arturstepkowski.pl
Nie używaj Optional jako typu argumentu wywołania
funkcji
Zasada ta dotyczy także konstruktorów i setterów. Użycie Optional w tym kontekście
powoduje niepotrzebną komplikację kodu oraz wymusza umieszczanie parametrów w
kontenerze nawet jeżeli jest to z oczywistych powodów zbyteczne.
www.arturstepkowski.pl
// Zła wersja metody sendEmail
public void sendEmail(Optional<User> userOpt) {
User user = userOpt.orElseThrow(() -> new
IllegalArgumentException("Użytkownik nie jest ustawiony");
sendEmail(user.getEmail());
}
// wywołanie metody - widać zbędne opakowanie w Optional
User defaultUser = new User("Default", "kontakt@mycompany.com");
sendEmail(Optional.of(defaultUser));
www.arturstepkowski.pl
// Właściwa wersja metody
public void sendEmail(User user) {
if(null == user) {
throw new IllegalArgumentException("Użytkownik nie jest
ustawiony");
}
sendEmail(user.getEmail());
// ...
}
// wywołanie metody - nie mamy zbędnego opakowywania
User defaultUser = new User("Default", "kontakt@mycompany.com");
sendEmail(defaultUser);
www.arturstepkowski.pl
Nie używaj Optional kiedy można zwrócić pustą
kolekcję
// ŹLE
public Optional<List<String>> getProductsNames(Order order) {
List<String> products = order.getProducts(); // lista produktów może być null
return Optional.ofNullable(products);
}
// DOBRZE
public List<String> getProductsNames(Order order) {
List<String> products = order.getProducts(); // lista produktów może być null
return products == null ? Collections.emptyList() : products;
}
Ta zasada sprawia, że kod jest czytelniejszy, prostszy i mniej podatny na błędy.
www.arturstepkowski.pl
Nie używaj Optional w kolekcjach
Ta zasada wynika z dwóch rzeczy:
1. Unikanie niepotrzebnej komplikacji kodu
2. Użycie Optional nie jest darmowe, kosztuje czas i pamięć. Szczególnie będzie to
widoczne w dużych kolekcjach.
www.arturstepkowski.pl
Nie używaj generycznej klasy Optional<T> dla typów
prostych
Użycie klasy generycznej Optional, jak wspomniano wcześniej, jest obarczone
kosztem. Żeby zminimalizować ten koszt w przypadku typów prostych używaj
dedykowanych klas kontenerów:
● OptionalInt
● OptionalLong
● OptionalDouble
www.arturstepkowski.pl
Dziękuję za uwagę
ArturStepkowskiDev
Artur Stępkowski
artiste_dev

Optional - nigdy więcej NullPointerException

  • 1.
    www.arturstepkowski.pl Optional - nigdywięcej NullPointerException Użycie i dobre praktyki
  • 2.
    www.arturstepkowski.pl O czym będzie? 1.Jak sobie radziliśmy przed Java 1.8? 2. Jak używać kontenera Optional 3. Dobre praktyki stosowania Optional
  • 3.
  • 4.
    www.arturstepkowski.pl return null public MyObjectget(final long id) { ... if(resultSet.next()) { return prepareMyObject(rs); } return null; }
  • 5.
    www.arturstepkowski.pl Chyba najgorsze zmożliwych rozwiązań. To było proszenie się o kłopoty. NullPointerException był praktycznie oczywistością. O ile jeszcze twórca funkcji pobierającej dane wiedział, że zwraca ona null i mógł się przed tym zabezpieczyć odpowiednim if-em, to każdy inny programista wcześniej czy później natrafiał na NPE.
  • 6.
    www.arturstepkowski.pl throw exception public MyObjectget(final long id) throws MyObjectNotFoundException { ... if(resultSet.next()) { return prepareMyObject(rs); } throw new MyObjectNotFoundException(); }
  • 7.
    www.arturstepkowski.pl Rozwiązanie lepsze, bonie ma już ryzyka NPE, ale używanie wyjątków w sytuacjach możliwych i naturalnych w aplikacji jest dyskusyjne, a dodatkowo w każdym przypadku trzeba napisać bloki try-catch.
  • 8.
    www.arturstepkowski.pl Wzorzec NullObject public abstractclass AbstractMyObject { protected String name; public abstract String getName(); public abstract void setName(String name); // reszta koniecznych metod } Stosując ten wzorzec w pierwszej kolejności musimy utworzyć klasę abstrakcyjną.
  • 9.
    www.arturstepkowski.pl public class MyObjectextends AbstractMyObject { @Override public String getName() { return name; } @Override public void setName(String name) { this.name = name; } } Następnie tworzymy klasę główną, która rozszerza nam tę klasę abstrakcyjną...
  • 10.
    www.arturstepkowski.pl public class NullObjectextends AbstractMyObject { @Override public String getName() { return ""; // zwracamy wartość domyślną } @Override public void setName(String name) { // metoda pusta } } oraz klasę “nullową”, która także rozszerza wspomnianą klasę abstrakcyjną.
  • 11.
    www.arturstepkowski.pl To rozwiązanie symulowałoczęściowo to, co dostaliśmy po wprowadzeniu Optional<T>. Brak ryzyka NPE, brak niepotrzebnej obsługi wyjątków, dodatkowo można w tym rozwiązaniu odwoływać się do metod interesującej nas klasy. Minusy? ● Dla każdego przypadku musimy tworzyć klasę abstrakcyjną, właściwą klasę dla obiektów oraz wydmuszkę NullObject ● Problem z rozszerzalnością. Za każdym razem musimy edytować trzy klasy ● Brak wiedzy o wartości zwracanej przez metody klasy NullObject ● wykorzystanie tego wzorca w przypadku takich klas jak String, Integer, Boolean itp. komplikuje kod
  • 12.
  • 13.
    www.arturstepkowski.pl Odkąd dostępna jestklasa Optional nie musimy się już martwić opisanymi przed chwilą problemami. Zwracana wartość jest opakowywana w kontener, a zamiast null zwracamy Optional.empty(). W tym momencie otrzymujemy samodokumentujący się kod. Inny programista (albo my sami po tygodniu) od razu będzie widział w jaki sposób obsłużyć zwracaną wartość.
  • 14.
    www.arturstepkowski.pl public Optional<MyObject> get(finallong id) { // ... if(resultSet.next()) { return Optional.of(prepareMyObject(rs)); } return Optional.empty(); } Przykład opakowania zwracanego obiektu w Optional<T>
  • 15.
    www.arturstepkowski.pl Co dalej? Jak używaćobiektu Optional? Jakie metody można na nim wywoływać?
  • 16.
    www.arturstepkowski.pl isPresent() – Sprawdzenieczy obiekt ma ustawioną wartość Optional<MyObject> myObject = getMyObject(); if(myObject.isPresent()) { // ... } Od Java 11 mamy do dyspozycji metodę isEmpty(), która, jak można się domyślić, pozwala na sprawdzenie, czy kontener Optional jest pusty.
  • 17.
    www.arturstepkowski.pl get() – pobraniewartości z kontenera Optional<MyObject> opt = getMyObject(); if(opt.isPresent()) { MyObject myObject = opt.get(); // ... } Jeżeli Optional zawiera wartość, to jest ona zwracana. Użycie metody get() powinno być poprzedzone sprawdzeniem czy Optional jest niepusty lub zastąpione użyciem metody orElse() (patrz dalej).
  • 18.
    www.arturstepkowski.pl orElse(T other) –pobranie wartości domyślnej Optional<String> opt = geUserFullName(); String userFullName = opt.orElse(""); Użycie metody orElse powoduje zwrócenie zawartości kontenera Optional jeżeli ta jest ustawiona lub podanej wartości domyślnej, jeżeli kontener jest pusty. Wartość domyślna powinna być przemyślana. Ustawienie jej na null w sporej części przypadków nie będzie dobrym rozwiązaniem. Jak można zauważyć użycie orElse skraca kod i jednocześnie czyni go czytelniejszym.
  • 19.
    www.arturstepkowski.pl filter() - filtrowanie,zamiennik if Optional<MyObject> opt = getMyObject(); if(opt.isPresent()) { MyObject myObject = opt.get(); if(myObject.getValue() == 10) { return myObject.getName(); } } Zamiast:
  • 20.
    www.arturstepkowski.pl Optional<MyObject> opt =getMyObject().filter(o->o.getValue()== 10); if(opt.isPresent()) { return opt.get() .getName(); } lepiej jest napisać:
  • 21.
    www.arturstepkowski.pl Mapowanie Optional<String> opt =getMyObject().map(o->o.getName()); if(opt.isPresent()) { return opt.get(); } Ta funkcjonalność pozwala nam na zmapowanie obiektu z kontenera w inny. Najczęściej stosuje się to po to, żeby wydobyć wartość konkretnych pól.
  • 22.
    www.arturstepkowski.pl Optional<String> opt =getUser().flatMap(u->u.getNickname()); String nickname = opt.orElse(""); A co w przypadku, gdy funkcja użyta w map zwraca Optional? Dostaniemy Optional<Optional<T>>. Niezbyt ciekawa perspektywa, ale na szczęście mamy funkcję flatMap na takie sytuacje. Rozważmy klasę User, gdzie metoda getNickname() zwraca Optional<String> (użytkownik może mieć ksywkę, ale nie musi).
  • 23.
  • 24.
    www.arturstepkowski.pl Jako argumentu orElsenie używaj funkcji Optional<MyObject> opt = getMyObject(); MyObject myObject = opt.orElseGet(()->getDefaultValue()); Metoda orElse ma taki drobny feler, że jeżeli jako parametru wywołania użyjemy funkcji to zostanie ona zawsze wywołana, nawet jeśli Optional zawiera wartość. Zwłaszcza w przypadku kiedy wywoływana funkcja jest kosztowna czasowo, to znacznie lepiej jest wykorzystać metodę orElseGet().
  • 25.
    www.arturstepkowski.pl Nigdy nie przypisujwartości null do zmiennej Optional Optional<String> opt = null; // ŹLE Optional<String> opt = Optional.empty(); // DOBRZE Optional jest kontenerem i przypisywanie zmiennym tego typu wartości null jest kompletnie bez sensu.
  • 26.
    www.arturstepkowski.pl Jeśli musisz wykonaćdziałanie tylko wtedy kiedy wartość w kontenerze jest ustawiona użyj ifPresent // ŹLE Optional<String> opt = getUser().map(u->u.getEmail()); if(opt.isPresent()) { new EmailSender().send(opt.get()); } // DOBRZE Optional<String> opt = getUser().map(u->u.getEmail()); opt.ifPresent(e->new EmailSender().send(e)); Rozważmy sytuację, kiedy chcemy wysłać maila tylko wtedy kiedy user ma ustawiony adres e-mail.
  • 27.
    www.arturstepkowski.pl Nie nadużywaj Optionalw prostych sytuacjach // NIEWŁAŚCIWIE Long counter = count(); return Optional.ofNullable(counter).orElse(0L); // ZNACZNIE LEPIEJ Long counter = count(); return null == counter ? 0L : counter; Jeżeli chcemy sprawdzić czy zmienna przenosząca wartość jest null i ustawić wtedy wartość domyślną, nie powinniśmy w większości wypadków używać Optional. Przykładowo:
  • 28.
    www.arturstepkowski.pl Nie deklaruj pólw klasie jako Optional Zasada ta dotyczy zwłaszcza tych klas, które podlegają serializacji, gdyż klasa Optional nie implementuje interfejsu Serializable. Ale uważa się, że zasadę tę należy stosować do wszystkich klas, z dwóch powodów: ● Warto stosować jeden schemat tworzenia klas, niezależnie od tego czy są serializowalne czy też nie. ● Stworzenie w klasie pola typu Optional<T> tak naprawdę nie daje nam żadnej przewagi. Jeżeli klasa przenosi dane, to w większości wypadków będzie serializowalna. Jeżeli odpowiada za logikę biznesową, to użycie w niej zmiennej globalnej samo w sobie nie jest dobrą praktyką
  • 29.
    www.arturstepkowski.pl public class User{ private String nickname; // pole bez Optional // ... // getter zwraca Optional public Optional<String> getNickname() { return Optional.ofNullable(nickname); } // ... } Przykład:
  • 30.
    www.arturstepkowski.pl Nie używaj Optionaljako typu argumentu wywołania funkcji Zasada ta dotyczy także konstruktorów i setterów. Użycie Optional w tym kontekście powoduje niepotrzebną komplikację kodu oraz wymusza umieszczanie parametrów w kontenerze nawet jeżeli jest to z oczywistych powodów zbyteczne.
  • 31.
    www.arturstepkowski.pl // Zła wersjametody sendEmail public void sendEmail(Optional<User> userOpt) { User user = userOpt.orElseThrow(() -> new IllegalArgumentException("Użytkownik nie jest ustawiony"); sendEmail(user.getEmail()); } // wywołanie metody - widać zbędne opakowanie w Optional User defaultUser = new User("Default", "kontakt@mycompany.com"); sendEmail(Optional.of(defaultUser));
  • 32.
    www.arturstepkowski.pl // Właściwa wersjametody public void sendEmail(User user) { if(null == user) { throw new IllegalArgumentException("Użytkownik nie jest ustawiony"); } sendEmail(user.getEmail()); // ... } // wywołanie metody - nie mamy zbędnego opakowywania User defaultUser = new User("Default", "kontakt@mycompany.com"); sendEmail(defaultUser);
  • 33.
    www.arturstepkowski.pl Nie używaj Optionalkiedy można zwrócić pustą kolekcję // ŹLE public Optional<List<String>> getProductsNames(Order order) { List<String> products = order.getProducts(); // lista produktów może być null return Optional.ofNullable(products); } // DOBRZE public List<String> getProductsNames(Order order) { List<String> products = order.getProducts(); // lista produktów może być null return products == null ? Collections.emptyList() : products; } Ta zasada sprawia, że kod jest czytelniejszy, prostszy i mniej podatny na błędy.
  • 34.
    www.arturstepkowski.pl Nie używaj Optionalw kolekcjach Ta zasada wynika z dwóch rzeczy: 1. Unikanie niepotrzebnej komplikacji kodu 2. Użycie Optional nie jest darmowe, kosztuje czas i pamięć. Szczególnie będzie to widoczne w dużych kolekcjach.
  • 35.
    www.arturstepkowski.pl Nie używaj generycznejklasy Optional<T> dla typów prostych Użycie klasy generycznej Optional, jak wspomniano wcześniej, jest obarczone kosztem. Żeby zminimalizować ten koszt w przypadku typów prostych używaj dedykowanych klas kontenerów: ● OptionalInt ● OptionalLong ● OptionalDouble
  • 36.