Java 8: Optional prieš null    

Taip pat siūlome susipažinti: Anotacijos Java kalboje  
Lambda išraiškos – Java į naują lygį  

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?
null 1965 m. įvedė britas Tony Hoare projektuodamas ALGOL W, vieną pirmųjų programavimo kalbų, naudojančių tipus, turinčią įrašų išskyrimą „iš krūvos“ – nes „tai taip lengva realizuoti“. Priešingai savo iškeltam tikslui „užtikrinti, kad visos nuorodos būtų absoliučiai saugios, automatiškai patikrinamos kompiliatoriaus“, jis nusprendė išimtį padaryti dėl null, nes manė, kad tai patogausias būdas nurodyti reikšmės nebuvimą. Po daugelio metų jis pasmerkė tą savo sprendimą, pavadindamas jį „milijardo vertės klaida“ – ir mes visi regim jos poveikį mums.

Tad ir pabandykime iš arčiau pažiūrėti, kokias problemas sukelia null.
Paimkim įdėtinių klasių rinkinį:

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)?

Taip 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 car = Optional.empty();

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);

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.

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:
PasaulėflatMap procese

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;
* orElse(T other) - jau mūsų naudotas metodas. Jis numato nutylimąją reikšmę, kai objektas yra tuščias;
* orElseGet(Supplier<? extends T> other) – „tinginio variantas“. Supplier iškviečiamas tik objektui neturint reikšmės. Jis naudotinas, kai nutylimoji reikšmė kuriama lėtai (trputis ekonomijos) arba norite būti užtikrinti, kad tai bus padaryta tik jei objektas tuščias (šiuo atveju tai griežtai būtina);
* orElseThrow(Supplier<? extends X> exceptionSupplier) – panašus į get, tačiau leidžia pasirinkti išimties tipą;
* ifPresent(Consumer<? super T> consumer) – nurod, kad reikia atlikti veiksmą su turima reikšme. Nieko nedaroma, jei reikšmės nėra.

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 realzacija pernelyg artima null patikroms. Ar nėra geresnio būdo?
Prisiminus ankstesnį flatMap ir map kombinavimą, perrašom elegantiškai:

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.

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ą.


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");

	ProcedeNULL app = new ProcedeNULL();
	Person petras = app.new Person();
        System.out.println(getCarInsurance(petras));
   
	System.out.println("Sudie, 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; }
}
	
}

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.HashMap;
import java.util.Map;
import java.util.Optional;
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("\nSudie, Optional");

} // -- end main

public static Optional<Integer> stringToInt (final String pStr) {
	try {
		return Optional.of(Integer.parseInt(pStr));
	} catch ( NumberFormatException e ) { 
		return Optional.empty();
	}
}

static String getCarInsurance(Optional<Person> person) {
	    return person.flatMap(Person::getCar)
	            .flatMap(Car::getInsurance)
	            .map(Insurance::getName)
	            .orElse ( coUnsetText );
}


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( coUnsetText );
}


// Atrankos pvz.
public Insurance pigiausiasDraudikas(Person person, Car car) {
	Insurance piguva = null;
	// veiksmai
	return piguva;
}

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();
}

public Optional<Insurance> pigiausiasDraudikasLambda(Optional<Person> person, Optional<Car> car) {
	return person.flatMap(p -> car.map(c -> pigiausiasDraudikas(p, c)));
}

// --------------------------------------------------------------------
private class Person {	
	private Car car = null;
	private Optional<Car> optCar = Optional.empty();
	private int age = 0;
	
	public Optional<Car> getCar() { return optCar; }
	public Optional<Car> getCarOptionally() { return Optional.ofNullable(car); }
	public void setCar(Car pCar) {
		car = pCar;
		optCar = Optional.ofNullable(car);
	}
	
	public int getAge() {
		return age;
	}
}
	
private class Car {
    private Optional<Insurance> insurance;
    public Optional<Insurance> getInsurance() { return insurance; }
}
    
private class Insurance {
    private String name;
    public String getName() { return name; }
    public void setName(String pName) {  name = pName; }
}


private class Savybes {
	Properties props = new Properties();
	
	public void add (String pName, String pValue) {
		props.setProperty(pName, pValue);
	}
	
	public int kiekTrunka_Senobinis(String pName) {
		String value = props.getProperty(pName);
		if (value != null) {
			try {
				int i = Integer.parseInt(value);
				if (i > 0) return i;
			} catch ( NumberFormatException e) {}
		}
		return 0;
	}
	
	public int kiekTrunka (String pName) {		
		return Optional.ofNullable(props.getProperty(pName))
                .flatMap(LabasOptional::stringToInt)
                .filter(i -> i > 0)
                .orElse(0);		
	}
}

}

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

Sudie, Optional

(c) 2016, 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ūūūū...
Tiesiog - Java
'Java' ir ne tik ji!
Skriptai - ateities kalbos?
JavaScript pradžiamokslis
Anotacijos Java kalboje
Programavimas Unix aplinkoje
Programavimo kalbų klegesys
Pitonas, kandantis sau uodegą!
AWK kalba - sena ir nuolat aktuali
Lambda išraiškos – Java į naują lygį
CGI.pm biblioteka: sausainiai
Vaizdi rašysena - VB Script
Programavimo kalbų istorija
Dygios JavaScript eilutės
Unix komandinė eilutė
Ruby on Rails
AdvancedHTML
Vartiklis