Java 8: Optional prieš null Taip pat siūlome susipažinti: Anotacijos Java kalboje Kuris iš Java programuotojų nėra gavęs NullPointerException? Ir
tikriausiai dažnas jų pagalvoja: Nu, jo, tai galvos skausmas kiekvienam
programuotojui, naujokui ir patyrusiam, ir čia daug nieko nepadarysi
tai kaina, kurią turim sumokėti už šį patogų, tačiau kaprizingą null konstruktą. Kokios jo ištakos? Tad ir pabandykime iš arčiau pažiūrėti, kokias problemas sukelia null. class Person { private Car car; public Car getCar() { return car; } } class Car { private Insurance insurance; public Insurance getInsurance() { return insurance; } } class Insurance { private String name; public String getName() { return name; } } Bet kas blogai realizavus tokį metodą? String getCarInsurance(Person person) { return person.getCar().getInsurance().getName(); } Atrodo labai normaliai. Tačiau ne visi asmenys turi automobilius, o kai kurie jų ir nedraudžia (nors ir privalėtų!). Tad koks bus getCar() rezultatas? Nelaimei, bendra praktika yra gražinti null reikšmę tuo pažymint automobilio neturėjimą. Bet tokiu atveju tolimesnis vykdymas sukels NullPointerException situaciją. Bet ir tai dar ne viskas o kas, jei ir person reikšmė yra null (o dar yra ir getInsurance(), taip pat galintis gražinti null)? Kaip apsisaugoti nuo to? Paprastai, visur, kur būtina (o kartais, apsidraudžiant, net ir kur nebūtina), įdedami null patikrinimai. Tad pirmas bandymas perrašyti nesaugų metodą būtų: String getSafeCarInsurance(Person person) { if (person != null) { Car car = person.getCar(); if (car != null) { Insurance insurance = car.getInsurance(); if (insurance != null) return insurance.getName(); } } return "Nenustatyta"; } Šį variantą pavadinkime gilia abejone, nes jis parodo pasikartojantį elgesį kaskart suabejojama, ar kintamasis turi reikšmę. Galiausiai gauname griozdišką kelių lygių kodą. Tad pabandykime kitaip: String getCarInsuranceUsingExits(Person person) { if (person == null) return "Nenustatyta"; Car car = person.getCar(); if (car == null) return "Nenustatyta"; Insurance insurance = car.getInsurance(); if (insurance == null) return "Nenustatyta"; return insurance.getName(); } Bet ir šis sprendimas tolimas nuo idealo: metode yra 4-i išėjimo taškai, iš jų triskart gražina reikšmę "Nenustatyta" ir gerai, jei nesuklysime parašydami tą reikšmę... na gerai, galite ją priskirti konstantai ir naudoti šios vardą. O toliau klaidoms imlus kodas o kas, jei pamiršime kuriai nors iš savybių patikrinti null reikšmę?! Ir toks sprendimas tėra purvo išpylimas ant kilimo. Jis nesprendžia klaidos kode, o tik maskuoja jos pasekmes, padarydamas jos suradimą sunkesniu ir ilgiau trunkančiu. O ir klaida gali pasireikšti tik po kelių mėnesių... Taigi, atrodo, kad null naudojimas nesančios reikšmės žymėjimui nėra geriausias būdas. O ką galime rinktis? Pasižvalgykime į kitas kalbas... Prieš kelis metus tokios kalbos kaip Groovy šią problemą sprendė įvesdamos saugios navigacijos operatorių ?. Pažvelkime, kaip Groovy kalboje būtų apeita minėta situacija: def carInsurance = person?.car?.insurance?.name Ką atlieka ši išraiška yra neabejotinai aišku. Pasitaikiusi null reikšmė nuburbuliuoja iki rezultato. Panaši galimybė buvo pasiūlyta Java 7, tačiau jos buvo atsisakyta. Kitos funkcionalinės kalbos (Haskell ar Scala) elgiasi kitaip. Haskell įtraukia Maybe tipą, iš esmės uždarantį nebūtiną reikšmę; ir nėra null koncepto. Scala turi panašią Option[T] konstrukciją, leidžiančią nebūtiną T tipo reikšmę; jos nebuvimą reikalaujama patikrinti, tad tai tik sustiprina null patikrinimo idėją. Nagi, visa tai atrodo gana abstrakčiai. Java 8 irgi pasiima nebūtinos reikšmės idėją per naują java.util.Optional<T> klasę. Jei objektas turi reikšmę, tiesiog apvynioja jį. Reikšmės nebuvimas modeliuojamas Optional.empty() metodu. Tai fabrikinis metodas, gražinantis Optional klasės specialų singletono įkūnijimą. Bet kuo tai skiriasi nuo null? Semantiškai tai gali atrodyti kaip tas pats dalykas, tačiau praktiškoje skirtumas milžiniškas. Kreipimasis per null neišvengiamai iššauks išimtį, o Optional.empty() yra validus, naudotinas Optional tipo objektas. Netrukus parodysime, kaip. Pirmiausia naujai susikuriame mūsų minėtas klases: private class Person { private Optional<Car> car; public Optional<Car> getCar() { return car; } } private class Car { private Optional<Insurance> insurance; public Optional<Insurance> getInsurance() { return insurance; } } private class Insurance { private String name; public String getName() { return name; } } O toliau pažiūrim, kaip susikuriamos Optional tipo objektai. Pirmas būdas naudoti Optional.empty() metodą: Optional Toliau, galima sukurti opcinę reikšmę iš turinčio reikšmę objekto (bet jei car reikšmė bus null, iškart gausime NullPointerException): Optional<Car> optCar = Optional.of(car); Ankstesnio minėto atvejo su null išvengimui galima panaudoti: Optional<Car> optCar = Optional.ofNullable(car); Papildomas perspėjimas: vis tik Optional.of metodo parametro reikšmė negali būti null - tokiu atveju gausime NullPointerException išimtį, pvz., String vardas = null; Optional<String> optVardas = Optional.of(vardas); Tačiau tokiu atveju mes galime naudoti: Optional<String> optVardas = Optional.ofNullable(vardas); kas priskiria tuščią Optional objektą. O ... kaip gauti Optional tipo reikšmę? Tam yra get metodas, tačiau jis iššaukia išimtį, kai objektas neturi reikšmės, tad jo naudojimas veda į visus anksčiau minėtus aspektus. Tipinis veiksmas yra gauti informaciją iš objekto. Tarkim: String name = null; if ( insurance != null ) { name = insurance.getName(); } Tam tikslui Optional turi map metodą, pvz.: Insurance insurance = new Insurance(); Optional<Insurance> optInsurance = Optional.ofNullable(insurance); Optional<String> name = optInsurance.map(Insurance::getName); Konceptualiai tai panašu į stream naudojamą map metodą, kur jis taiko numatytą funkciją kiekvienam srauto elementui. Tad Optional objektą galime įsivaizduoti kaip duomenų rinkinį, teturintį daugų daugiausia vieną elementą. Jei tasai turi elementą, funkcija jį perduota į map, kad toji transformuotų reikšmę. Jei objektas tuščias, niekas nenutinka. Papildoma pastaba: map metodas gražina skaičiavimo įvyniotame Optional rezultatą (Optional tipo), o tada galima panaudoti reikiamą metodą reikšmės gavimui sugražintame Optional objekte. Tai pailiustruosime tokiu pavyzdžiu: List<String> companies = Arrays.asList ("IBM", "Oracle", "Microsoft", ""); Optional<List<String>> optCompanies = Optional.of(companies); System.out.println("Kompanijos: " + app.getListLength(optCompanies)); optCompanies = Optional.empty(); System.out.println("Kiekis kai empty Optional: " + app.getListLength(optCompanies)); // ... public int getListLength(Optional<List<String>> pList) { return pList.map(List::size).orElse(0); } Pirmo kreipinio metu perduodamas kompanijų sąrašas, tada įvykdoma List metodas size()
(sąrašo ilgis) ir gražinamas Optional metodas šiai reikšmei. Tada orElse gražina šią sąrašo reikšmę (4). Bet vis tik, o kaip mums perrašyti mums reikiamą getCarInsurance funkciją? Mums gali kilti noras parašyti tokį kodą: Optional<Person> optPerson = Optional.of(person); Optional<String> name = optPerson.map(Person::getCar) .map(Car::getInsurance) .map(Insurance::getName); Gaila, jis nebus kompiliuojamas. Nors optPeople yra Optional<People> tipo, getCar() gražina Optional<Car> tipo reikšmę, tad map rezultatas bus Optional<Optional<Car>> tipo, o tada getInsurance kreipinys yra neleistinas dėl tipų nesuderinamumo. Ką daryti? Pasižiūrėkime į dar vieną Optional funkciją flatMap, kurią taipogi turi ir srautai. Ji tarsi suplokština map rezultatą. Tad rašom: String getCarInsurance(Optional<Person> person) { return person.flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse ("Nenustatyta"); } Gavome siekiamą tikslą nenaudodami daugybės if. Taip pat šis
sprendimas vaiskus tipų požiūriu aiškiai įvardija, kad reikšmės gali ir
nebūti. Jis gali būti grafiškai iliustruotas taip: Taigi, parodėme, kaip elgtis su objektais, galinčiais neturėti reikšmės. Tačiau Optional klasė buvo kuriama kitais sumetimais. Pvz., Java kalbos architektas Brian Goetz'as aiškiai teigia, kad Optional skirta tik opcinio gražinimo idiomai. Tad, kadangi nemanyta jos naudoti lauko tipui, ji neįdiegia Serializable interfeiso. Tad tokio poreikio užtikrinimui realizuoti papildomą metodą, gražinantį neprivalomą reikšmę, pvz., public Optional<Car> getCarOptionally() { return Optional.ofNullable(car); } Nutylimosios reikšmės ir Optional išvyniojimas Optional klasė numato kelis metodus reikšmės nuskaitymui: * get paprasčiausias, tačiau nesaugiausias reikšmės paėmimo būdas.
Jis išmeta NoSuchElementException išimtį, jei objektas yra tuščias. Tad
su juo netoli pasistūmėsim lyginant su null patikromis; Tarkim, reikia metodo su painia logika, kuris pagal nurodytą asmenį ir automobilį, surastų pigiausią draudimo kompaniją. Jo skeletas būtų toks: public Insurance pigiausiasDraudikas(Person person, Car car) { Insurance piguva = null; // veiksmai return piguva; } O jei norime, kad jis būtų saugus null atžvilgiu, parengiam tokį jo įvyniojimą: public Optional<Insurance> pigiausiasDraudikasSaugiai(Optional<Person> person, Optional<Car> car) { if (person.isPresent() && car.isPresent()) return Optional.of(pigiausiasDraudikas(person.get(), car.get())); return Optional.empty(); } Bet tokia realizacija pernelyg artima null patikroms. Ar nėra geresnio būdo? public Optional<Insurance> pigiausiasDraudikasLambda(Optional<Person> person, Optional<Car> car) { return person.flatMap(p -> car.map(c -> pigiausiasDraudikas(p, c))); } Šiuo atveju lambda išraiška nebus vykdoma, jei Person objektas yra tuščias. O lambda išraiškos kūne, jei nėra jokio automobilio, gražinamas tuščias Optional objektas. Panašumai su srautais dar nesibaigė. Bendras su jais yra ir filter metodas. Jį panaudosime atrenkant draudikus, kurių pavadinimas yra "Drauda": optInsurance.filter(dr -> dr.getName().equals("Drauda")) .ifPresent(x -> System.out.println("ok")); O dabar tarkim, kad asmens klasė turi metodą getAge(), pateikiantį asmens amžių. Adaptuokime kompanijos paieškos metodą, atsižvelgiant į draudžiamojo amžių: public String getCarInsuranceOnAge(Optional<Person> person, int minAge) { return person.filter(p -> p.getAge() >= minAge) .flatMap(Person::getCar) .flatMap(Car::getInsurance) .map(Insurance::getName) .orElse( Nenustatyta ); } Apibendrinant galima pasakyti, kad Optional klasė priverčia permąstyti, kaip elgtis su potencialiai negarantuotomis reikšmėmis. Tai gali paveikti ir sąveiką su pačia Java API. Vienok, suderinamumui su senuoju kodu, Java API liko nepakeista. Bet ir čia mes galime sužaisti, apvyniodami tokius API kreipinius. Tarkim, map reikšmė tebegražina null, jei rakto reikšmė nesurandama. Panaudoti Optional galima ir išimčių valdyme. Tarkim, teksto eilutės vertimas į sveiką skaičių iššaukia NumberFormatException išimtį, jei eilutėje nėra išgliaudomo skaičiaus. Išsprendžiama apgaubiant: public static Optional<Integer> stringToInt (final String pStr) { try { return Optional.of(Integer.parseInt(pStr)); } catch ( NumberFormatException e ) { return Optional.empty(); } } Beje, kaip ir srautai, Optional turi savas variacijas: OptionalInt, OptionalLong ir OptionalDouble. Jas jau paminėjime pristatydami srautus lambda išraiškose. Tačiau neskatintumėm jų naudoti, nes iš jų iškastruoti map, flatMap ir filter metodai. Vis tik elkitės atsargiai Šio straipsnelio pavyzdžiuose kai kurie metodai parametruose naudojo Optional tipą (getCarInsurance, pigiausiasDraudikasSaugiai, pigiausiasDraudikasLambda, getCarInsuranceOnAge). Tačiau iš tikro jo įtraukimo į Java intencija buvo jį panaudoti kaip return, o ne parametro tipą. Minėtų metodų atveju turime pavojų, kad jie bus iškviesti neteisingai. Įsivaizduokite, kad jūs sukūrėte straipsnyje minėtą getCarInsuranceOnAge metodą, o kitas programuotojas jį iškvietė taip: Person person = null; String getCarInsuranceOnAge(person, 25); Aišku, jis tada gautų NullPointerException ir jus išvadintų pačiais gražiausiais žodžiais. Tad Optional naudojimas parametrams net nėra rekomenduojamas kai kurių kodo analizatorių. Apibendrinant O dabar sudėkime viską, ką sužinojome. Tam susikurkime savybių (Properties) rinkinį. Tarkim, kad jos perteikia trukmę sekundėmis tad tai turi būti sveikas skaičius. Metodas kiekTrunka turi pateikti savybės reišmę, bet jei ši nėra sveikas skaičius, gražinti 0. Pavyzdį realizuojame kaip klasę: private class Savybes { Properties props = new Properties(); // ... public int kiekTrunka (String pName) { return Optional.ofNullable(props.getProperty(pName)) .flatMap(LabasOptional::stringToInt) .filter(i -> i > 0) .orElse(0); } } Pilnas veikiantis kodas, iliustruojantis straipsnelyje aptariamus klausimus, pateikiamas prieduose. Jų vykdymui naudokite Java 8 aplinką. Pastaba: Java 9 papildė Optional naujais metodais: Priedai 1. Žaidimas su null Pateikiame pilną pradinį pavyzdžio, iliustruojančio straipsnyje aprašytas problemas su null, tekstą. Pastaba: Jo vykdymui naudokite Java 8 aplinką. public class ProcedeNULL { public static void main(String[] args) { System.out.println("Labas, Optional!/n"); ProcedeNULL app = new ProcedeNULL(); Person petras = app.new Person(); try { System.out.println(getCarInsurance(petras)); } catch (Exception e) { System.out.println("Gavom NPE"); } System.out.println(getSafeCarInsurance(petras)); System.out.println(getCarInsuranceUsingExits(petras)); System.out.println("\nSudie, Optional!"); } static String getCarInsurance(Person person) { return person.getCar().getInsurance().getName(); } static String getSafeCarInsurance(Person person) { if (person != null) { Car car = person.getCar(); if (car != null) { Insurance insurance = car.getInsurance(); if (insurance != null) return insurance.getName(); } } return "Nenustatyta"; } static String getCarInsuranceUsingExits(Person person) { if (person == null) return "Nenustatyta"; Car car = person.getCar(); if (car == null) return "Nenustatyta"; Insurance insurance = car.getInsurance(); if (insurance == null) return "Nenustatyta"; return insurance.getName(); } // -------------------------------------------------------------------- private class Person { private Car car; public Car getCar() { return car; } } private class Car { private Insurance insurance; public Insurance getInsurance() { return insurance; } } private class Insurance { private String name; public String getName() { return name; } } } Jos vykdymo rezultatai: Labas, Optional Gavom NPE Nenustatyta Nenustatyta Sudie, Optional 2. Susipažinkime su Optional Pateikiame pilną pradinį pavyzdžio, iliustruojančio straipsnyje aprašytas problemas su null, tekstą. Pastaba: Jo vykdymui naudokite Java 8 aplinką. Vykdymo rezultatai pabaigoje. import java.util.Optional; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; public class LabasOptional { final static String coUnsetText = "Nenustatyta"; public static void main(String[] args) { System.out.println("Labas, Optional\n"); LabasOptional app = new LabasOptional(); Person petras = app.new Person(); Optional<Person> mikas = Optional.ofNullable(petras); Insurance insurance = app.new Insurance(); insurance.setName("Drauda"); Optional<Insurance> optInsurance = Optional.ofNullable(insurance); Optional<String> name = optInsurance.map(Insurance::getName); System.out.println(getCarInsurance(mikas)); optInsurance.filter(dr -> dr.getName().equals("Drauda")) .ifPresent(x -> System.out.println("ok")); Map<String, String> map = new HashMap<String, String>(); map.put("spyna", "kambarys"); Optional<Object> value = Optional.ofNullable(map.get("raktas")); System.out.println("Gavom: " + value); String str = "melagis"; System.out.println("Sveikasis: " + stringToInt(str) ); Savybes sav = app.new Savybes(); sav.add("vienas", "1"); sav.add("true", "true"); sav.add("minustrys", "-3"); sav.add("penki", "5"); System.out.println("vienas trunka: " + sav.kiekTrunka("vienas")); System.out.println("true trunka: " + sav.kiekTrunka("true")); System.out.println("minustrys trunka: " + sav.kiekTrunka("minustrys")); System.out.println("keturi trunka: " + sav.kiekTrunka("keturi")); System.out.println("penki trunka: " + sav.kiekTrunka("penki")); System.out.println("\nPapildomai:"); app.testNPE(); try { app.getCarInsuranceOnAge(mikas, null); } catch (Exception e) { System.out.println("NPE, kai kviesta su Optional parametru lygiu null"); } List<String> companies = Arrays.asList ("IBM", "Oracle", "Microsoft", ""); Optional<List<String>> optCompanies = Optional.of(companies); System.out.println("Kompanijos: " + app.getListLength(optCompanies)); optCompanies = Optional.empty(); System.out.println("Kiekis kai empty Optional: " + app.getListLength(optCompanies)); System.out.println("\nSudie, Optional"); } // -- end main public void testNPE() { String vardas = null; try { Optional Vykdymo rezultatai: Labas, Optional Nenustatyta ok Gavom: Optional.empty Sveikasis: Optional.empty vienas trunka: 1 true trunka: 0 minustrys trunka: 0 keturi trunka: 0 penki trunka: 5 Papildomai: NPE, kai Optional.of(null) NPE, kai kviesta su Optional parametru lygiu null Kompanijos: 4 Kiekis kai empty Optional: 0 Sudie, Optional (c) 2016, 2019. Vartiklis. Visos teisės saugomos. Leidžiama naudoti tik asmeninės savišvietos tikslais. Bet koks platinimas bet kokiomis priemonemis, viso teksto arba atskiros jo dalies, draudžiamas!
Ka-ka-rie-kūūūū... | |