Java 8 Optional - kompletan vodic, elegantno resenje za NullPointerException
Za ucenje nikada nije kasno, posebno kada su u pitanju dobri sadrzaji u Javi 8. Optional je jedan od njih — ova klasa pruza resenje na nivou klase za predstavljanje opcionalne vrednosti umesto null reference. Kao Java programer, iskreno sam zasiten NullPointerException-a (NPE); iako sam s njim toliko blizak kao sa starim prijateljem i razumem da on ne bira druge — program koristi objekat, a otkrije da je njegova vrednost null, pa ga Java virtuelna masina, prepuna besa, izbaci kao zrtvenog jarca.
Naravno, mi programeri smo odgovorni i ne mozemo samo gledati, pa se pojavilo mnogo provera na null. Iako takve provere ponekad uopste nisu neophodne, navikli smo da ih radimo rutinski. Konacno, u Javi 8 su odlucili da to vise ne mogu gledati i uveli su Optional kako nas kod ne bi bio toliko surov i krut.

01. Sta je problem bez Optional-a
Simulirajmo realan scenario. Xiao Wang je prvog radnog dana dobio zadatak od rukovodioca Stari Ma — da iz baze, na osnovu ID-a clana, izvuce ime clana i ispise ga na konzolu. Iako je nov, ovaj zadatak ga nije zaustavio, pa je za 10 minuta napisao sledeci kod:
public class WithoutOptionalDemo {
class Member {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static void main(String[] args) {
Member mem = getMemberByIdFromDB();
if (mem != null) {
System.out.println(mem.getName());
}
}
public static Member getMemberByIdFromDB() {
// clan sa ovim ID-em ne postoji
return null;
}
}Buduci da clan sa zadatim ID-em ne postoji, metoda getMemberByIdFromDB() vraca null kao rezultat kad se ne pronadje clan; to znaci da pre ispisivanja imena clana promenljivu mem treba proveriti na null, inace ce biti bacen NPE. Ne verujes? Neka Xiao Wang izbrise if (mem != null) i konzola ce mu odmah ispisati stek greske.
Exception in thread "main" java.lang.NullPointerException
at com.cmower.dzone.optional.WithoutOptionalDemo.main(WithoutOptionalDemo.java:24)02. Kako Optional resava ovaj problem
Xiao Wang je poslao kod i pun entuzijazma otrcao kod Starog Ma po novi zadatak. U duhu skromnog ucenja, zamolio je Starog Ma da pogleda njegov kod, a Stari Ma mu je rekao da proba sa Optional, cime se izbegavaju nepotrebne null provere. Hajde da vidimo kako je Xiao Wang pomocu Optional-a resio prethodni problem.
public class OptionalDemo {
public static void main(String[] args) {
Optional<Member> optional = getMemberByIdFromDB();
optional.ifPresent(mem -> {
System.out.println("Ime clana je: " + mem.getName());
});
}
public static Optional<Member> getMemberByIdFromDB() {
boolean hasName = true;
if (hasName) {
return Optional.of(new Member("Chenmo Wang Er"));
}
return Optional.empty();
}
}
class Member {
private String name;
public String getName() {
return name;
}
// getter / setter
}Metoda getMemberByIdFromDB() kao rezultat vraca Optional<Member>, cime se jasno naznacava da Member mozda postoji, a mozda i ne postoji. Tada se u metodi ifPresent() klase Optional moze koristiti Lambda izraz koji direktno ispisuje rezultat.
Optional resava problem NPE-a zato što nam izricito kaze da ga ne treba proveravati na null. On je kao znak na raskrsnici koji ti jasno pokazuje kuda da ides.
03. Kreiranje Optional objekata
- Statickom metodom
empty()moze se kreirati prazan Optional objekat:
Optional<String> empty = Optional.empty();
System.out.println(empty); // ispis: Optional.empty- Statickom metodom
of()moze se kreirati neprazan Optional objekat:
Optional<String> opt = Optional.of("Chenmo Wang Er");
System.out.println(opt); // ispis: Optional[Chenmo Wang Er]Naravno, argument prosledjen metodi of() mora biti neprazan, odnosno ne sme biti null, inace ce i dalje biti bacen NullPointerException.
String name = null;
Optional<String> optnull = Optional.of(name);- Statickom metodom
ofNullable()moze se kreirati Optional objekat koji moze biti i prazan i neprazan:
String name = null;
Optional<String> optOrNull = Optional.ofNullable(name);
System.out.println(optOrNull); // ispis: Optional.emptyMetoda ofNullable() interno ima ternarni izraz: ako je argument null, vraca privatnu konstantu EMPTY; u suprotnom, pomocu kljucne reci new kreira novi Optional objekat — vise nece baciti NPE.
04. Provera da li vrednost postoji
Metodom isPresent() mozes proveriti da li Optional objekat sadrzi vrednost; ako postoji, metoda vraca true, inace false — zamenjuje klasicnu proveru obj != null.
Optional<String> opt = Optional.of("Chenmo Wang Er");
System.out.println(opt.isPresent()); // ispis: true
Optional<String> optOrNull = Optional.ofNullable(null);
System.out.println(optOrNull.isPresent()); // ispis: falseOd Jave 11 mozes i metodom isEmpty() dobiti suprotan rezultat od isPresent().
Optional<String> opt = Optional.of("Chenmo Wang Er");
System.out.println(opt.isEmpty()); // ispis: false
Optional<String> optOrNull = Optional.ofNullable(null);
System.out.println(optOrNull.isEmpty()); // ispis: true05. Izraz za nepraznu vrednost
Klasa Optional poseduje veoma modernu metodu — ifPresent(), koja nam dozvoljava da koristimo funkcionalni stil za izvrsavanje nekog koda, pa je nazivam „izraz za nepraznu vrednost”. Bez te metode, obicno bismo prvo pomocu isPresent() proverili Optional objekat na null, a zatim izvrsili odgovarajuci kod:
Optional<String> optOrNull = Optional.ofNullable(null);
if (optOrNull.isPresent()) {
System.out.println(optOrNull.get().length());
}Sa ifPresent() situacija je potpuno drugacija — Lambda izraz se moze direktno proslediti toj metodi, pa je kod kraci i pregledniji.
Optional<String> opt = Optional.of("Chenmo Wang Er");
opt.ifPresent(str -> System.out.println(str.length()));Od Jave 9 mozes i metodom ifPresentOrElse(action, emptyAction) obraditi oba ishoda: kad vrednost postoji izvrsava se action, a kad je prazna izvrsava se emptyAction.
Optional<String> opt = Optional.of("Chenmo Wang Er");
opt.ifPresentOrElse(str -> System.out.println(str.length()), () -> System.out.println("prazno"));06. Postavljanje (uzimanje) podrazumevane vrednosti
Ponekad, kad kreiramo (ili uzimamo) Optional objekat, treba nam podrazumevana vrednost — tada na scenu stupaju metode orElse() i orElseGet().
Metoda orElse() vraca vrednost umotanu u Optional objekat: ako vrednost nije null, vraca nju; u suprotnom vraca podrazumevanu vrednost. Tip argumenta te metode podudara se sa tipom same vrednosti.
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("Chenmo Wang Er");
System.out.println(name); // ispis: Chenmo Wang ErMetoda orElseGet() je slicna metodi orElse(), ali joj se razlikuje tip parametra. Ako vrednost u Optional objektu bude null, izvrsava se funkcija prosledjena kao parametar.
String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(()->"Chenmo Wang Er");
System.out.println(name); // ispis: Chenmo Wang ErGledajuci rezultat i oblik koda, ove dve metode su izuzetno slicne, što budi sumnju — da li je dizajnerima Java biblioteke zaista bilo potrebno to da urade?
Pretpostavimo sada da imamo sledecu metodu koja vraca podrazumevanu vrednost, napisanu na veoma tradicionalan nacin.
public static String getDefaultValue() {
System.out.println("getDefaultValue");
return "Chenmo Wang Er";
}Zatim pomocu orElse() i orElseGet() pozivamo getDefaultValue() da bismo dobili podrazumevanu vrednost.
public static void main(String[] args) {
String name = null;
System.out.println("orElse");
String name2 = Optional.ofNullable(name).orElse(getDefaultValue());
System.out.println("orElseGet");
String name3 = Optional.ofNullable(name).orElseGet(OrElseOptionalDemo::getDefaultValue);
}Napomena: ImeKlase :: imeMetode je sintaksa uvedena u Javi 8; posle imena metode ne stoji (), što znaci da ta metoda ne mora obavezno biti pozvana.
Izlaz je sledeci:
orElse
getDefaultValue
orElseGet
getDefaultValueIzlazi su slicni i nema neke velike razlike — to je slucaj kada je vrednost Optional objekta null. Šta ako vrednost Optional objekta nije null?
public static void main(String[] args) {
String name = "Chenmo Wang San";
System.out.println("orElse");
String name2 = Optional.ofNullable(name).orElse(getDefaultValue());
System.out.println("orElseGet");
String name3 = Optional.ofNullable(name).orElseGet(OrElseOptionalDemo::getDefaultValue);
}Izlaz je sledeci:
orElse
getDefaultValue
orElseGetEto, orElseGet() nije pozvao getDefaultValue(). Koja metoda je bolja u pogledu performansi, jasno ti je, zar ne?
07. Uzimanje vrednosti
Sa semantickog stanovista, metoda get() je najautenticniji nacin da se uzme vrednost iz Optional objekta. Na zalost, ona ima manu: ako je vrednost Optional objekta null, baca izuzetak NoSuchElementException. To potpuno protivreci originalnoj nameni klase Optional.
public class GetOptionalDemo {
public static void main(String[] args) {
String name = null;
Optional<String> optOrNull = Optional.ofNullable(name);
System.out.println(optOrNull.get());
}
}Ovaj program pri pokretanju baca izuzetak:
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.base/java.util.Optional.get(Optional.java:141)
at com.cmower.dzone.optional.GetOptionalDemo.main(GetOptionalDemo.java:9)Iako je izbaceni izuzetak NoSuchElementException, a ne NPE, po nama je to „iz prve u prvu”. Preporucuje se upotreba orElseGet() za uzimanje vrednosti iz Optional objekta.
08. Filtriranje vrednosti
Xiao Wang je nadogradjuje prethodni kod pomocu klase Optional, a onda pun entuzijazma otrcao kod Starog Ma po novi zadatak. Stari Ma je zakljucio da je momak perspektivan — bistra glave i radan, pa vredi razvijati ga — i dao mu novi zadatak: prilikom registracije korisnika proveriti duzinu lozinke.
Xiao Wang se obradovao, jer je upravo hteo da nauci metodu filter() klase Optional — evo gde joj je mesto.
public class FilterOptionalDemo {
public static void main(String[] args) {
String password = "12345";
Optional<String> opt = Optional.ofNullable(password);
System.out.println(opt.filter(pwd -> pwd.length() > 6).isPresent());
}
}Tip parametra metode filter() je Predicate (funkcionalni interfejs uveden u Javi 8) — to znaci da joj se kao uslov moze proslediti Lambda izraz; ako rezultat tog izraza bude false, vraca se EMPTY Optional objekat, inace se vraca filtrirani Optional objekat.
U prethodnom primeru, buduci da je duzina password-a 5, program ispisuje false. Pretpostavimo da duzina lozinke mora biti izmedju 6 i 10 znakova — onda mozemo dodati jos jedan uslov. Pogledajmo kod pošto je Xiao Wang povecao tezinu.
Predicate<String> len6 = pwd -> pwd.length() > 6;
Predicate<String> len10 = pwd -> pwd.length() < 10;
password = "1234567";
opt = Optional.ofNullable(password);
boolean result = opt.filter(len6.and(len10)).isPresent();
System.out.println(result);Ovog puta program ispisuje true, jer lozinka sada ima 7 znakova — izmedju 6 i 10. Zamisli koliko bi kod bio preopsezan da je Xiao Wang iskoristio if-else za ovaj zadatak.
09. Konverzija vrednosti
Xiao Wang je zavrsio proveru duzine lozinke, ali mu je jos uvek bilo malo nedovoljno — zelio je da proveri i jacu lozinke; na primer, lozinka ne sme biti „password”, jer je takva lozinka previse slaba. Zato je poceo da proucava metodu map(), koja moze po odredjenom pravilu da transformise postojeci Optional objekat u novi Optional objekat, pri cemu se originalni Optional objekat ne menja.
Pogledaj prvo jednostavan primer koji je Xiao Wang napisao:
public class OptionalMapDemo {
public static void main(String[] args) {
String name = "Chenmo Wang Er";
Optional<String> nameOptional = Optional.of(name);
Optional<Integer> intOpt = nameOptional
.map(String::length);
System.out.println( intOpt.orElse(0));
}
}U ovom primeru, argument String::length metode map() znaci da se postojeci Optional tipa String transformise po pravilu duzine stringa u novi Optional objekat tipa Integer.
Pošto je razjasnio osnovnu upotrebu metode map(), Xiao Wang je odlucio da je kombinuje sa filter() — prva pretvara lozinku u mala slova, a druga proverava duzinu i to da li je lozinka „password”.
public class OptionalMapFilterDemo {
public static void main(String[] args) {
String password = "password";
Optional<String> opt = Optional.ofNullable(password);
Predicate<String> len6 = pwd -> pwd.length() > 6;
Predicate<String> len10 = pwd -> pwd.length() < 10;
Predicate<String> eq = pwd -> pwd.equals("password");
boolean result = opt.map(String::toLowerCase).filter(len6.and(len10 ).and(eq)).isPresent();
System.out.println(result);
}
}
To je to, drago moj citaocu — to je ceo sadrzaj ovog clanka, koji se moze nazvati najboljim mogucim vodicem za Optional. Ko je stigao dovde spada medju najbolje programere, pa ti Erge mora podici palac gore za tebe.
