2017. febr. 24. - Szerző: Forgács Ádám

Java Optional - Programozás egyenes síneken

A legtöbb programozási nyelvben probléma az érték hiányának kifejezése.

Vegyük azt az esetet, hogy felhasználó regisztrációt implementálunk egy alkalmazáson belül. Azonosításra az egyedi email cím szolgál. Mi az egyik első lépés, amit csinálunk? Megnézzük, hogy létezik-e az adatbázisban ilyen felhasználó. Mindkét esetet le kell kezelni. Ha nincs regisztrálva, akkor engedélyezzük az új felhasználó létrehozását. Ha van, akkor hibaüzenetet írunk ki. Ebben a példában az érték megléte és hiánya ugyanolyan helyzet, nem kívételes hiba. Tegyük fel, hogy van egy

	User getUserByEmail(String email);

metódusunk. Eddig az érték hiányát a legtöbb alkalommal úgy oldották meg, hogy null-al tért vissza a metódus (a Javán kívül más nyelvekben is, ha létezik bennük a null elem). Lehetőség van Exception-t is dobni. Ez utóbbi az adott példát nézve egy nem várt, ronda viselkedés. Az érték hiánya nem hiba! De a null-al visszatérés se a legszebb megoldás, a metódus azt írja le, hogy egy email címet megadva visszaad egy felhasználót, ami nem mindig teljesül. Az lenne a legjobb, hogy ha minden esetben tartani tudnánk a várt visszatérési értéket. A Java 8-ban bevezetett Optional<T> osztály erre nyújt megoldást.

Az Optional egy tároló osztály, ami 0 vagy 1 értéket tárolhat. Mivel generikus, ezért könnyedén megtudjuk szerezni a konkrét értéket. A bevezetésének az volt az elsődleges célja, hogy kifejezhessük, nem mindig van visszatérési érték. Egyéb metódusai is vannak, amikkel elég sok helyen lehet használni, ahogy az majd a példákban is látszódni fog. Az említett metódust át tudjuk írni az elábbira:

	private CriteriaQuery<User> buildQuery(String email) {
		final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
		final CriteriaQuery<User> query = cb.createQuery(User.class);
		final Root<User> user = query.from(User.class);
		query.where(cb.equal(user.get("email"), email));
		return query;
	}

	public User getUserByEmail_Old(String email) {

		final CriteriaQuery<User> query = buildQuery(email);
		return entityManager.createQuery(query).getSingleResult(); // Lehet null
	}

	public Optional<User> getUserByEmail_New(String email) {

		final CriteriaQuery<User> query = buildQuery(email);
		User user = entityManager.createQuery(query).getSingleResult();
		return Optional.ofNullable(user); // Mindenképp visszatérünk egy nem null objektummal
	}

Az Optional használatának meg van az előnye, hogy sokkal egyértelműbbé teszi az adott metódust, nem kell null-t ellenőrizni a hívóknak és emlékezteti őket, hogy lekezeljék mindegyik esetet. Természetesen fontos, hogy ne verjük át a metódus felhasználóit. Ha tudjuk, hogy biztosan visszatérünk valamivel, akkor nem szabad használni (pl. ha egy factory metódus mindig vissza tud adni egy objektum példányt). Illetve, ha az érték hiánya hibának számít, akkor valószínűleg a legjobb megoldás rögtön Exception-t dobni. A továbbiakban nézzük át a használatát. Ha van egy objektum referenciánk, akkor az of(…) vagy az ofNullable(…) metódusát használhatjuk a kovertáláshoz. Az előbbi hibával száll el, ha null-t adunk neki, érdemes mindig a másikat használni, hogy ilyenkor se kelljen null-t ellenőrizni. Ha egy üres Optional kell, akkor az empty() használható. Az esetleges értéket többféle módon is megszerezhetjük belőle. Egész pontosan négy metódusa is van erre: get(), orElse(…), orElseGet(…), orElseThrow(…). Úgy tűnne, hogy a get az egyértelmű választás, de ennek pont az ellenkezője az igaz. Mivel az Optional maga lehet üres, ezért a get csak akkor hívható biztonságosan, ha leellenőriztük az érték meglétét. Legrosszabb esetben NoSuchElementException-t dob. Ezzel a megközelítéssel nem nyerünk sokat, ahogy a példa mutatja:


	public void handleUser_Old() {
		User user = getUserByEmail_Old("[email protected]");
		if (user != null) {
			// user használata
		}
	}

	public void handleUser_New() {
		Optional<User> userValue = getUserByEmail_New("[email protected]");
		if (userValue.isPresent()) {
			User user = userValue.get();
			// user használata
		}
	}

Érdemesebb az orElse metódust használni, ami inkább funkcionális stílusú, vagyis mindenképp visszatér a definiált típusú értékkel. Ha az Optional üres, akkor a megadott másik értékkel tér vissza. Ha a másik érték kiszámolása költséges, akkor az orElseGet használható, ami az éppen érvényes generikus típusú Supplier-t várja. Ez természetesen csak szükséges esetben hívódik meg. Ha az érték hiánya hibának számít, akkor van még az orElseThrow, ami egy Throwable leszármazott típusú Supplier-t vár, amit az érték hiányakor meghív és eldobja a hibát. Ez egyáltalán nem funkcionális stílusú, de ha ezzel írhatjuk a legegyszerűbb kódot, akkor érdemes megfontolni a használatát. Néhány szemléltető példa:


	public void orElseExamples() {

		final String email = "[email protected]";

		// Default User objektumot használjuk, ha nincs
		User user = getUserByEmail(email).orElse(User.anonymous());

		// Ez így működik, ha szeretjük a null-t
		User nullableUser = getUserByEmail(email).orElse(null);

		// Ha nincs a gyorsítótárban, akkor az adatbázisból kérjük ki, de feleslegesen nem
		// akarunk behívni
		User otherUser = getUserFromCache(email).orElseGet(() -> getUserFromDatabase(email));

		// Hibának számít, úgyhogy azonnal hibát dobunk
		User existingUser = getUserByEmail(email).orElseThrow(IllegalArgumentException::new);
	}

Ha nincs szükségünk az érték referenciára, csak szeretnénk valamit lefuttatni, ha létezik, akkor használható az ifPresent(…) , ami egy Consumer-t vár, amit csak akkor hív meg az Optional, ha nem üres. Egyszerű példa erre:


	public void ifPresentExample() {
		final String email = "[email protected]";

		getUserByEmail(email).ifPresent(user -> {
			// user objektum használata
		});
	}

Sajnos nincs ifAbsent az Optional metódusai között. Ha szeretnénk valami logikát futtatni az érték hiányakor, akkor az isPresent értékét használhatjuk.

Az előző példák már megmutatták, hogy mennyire hasznos az Optional. Már ezekkel a bemutatott metódusokkal egy könnyen használható absztrakciót biztosít, megmentve a fejlesztőt a null értékek kezelésétől. Ami viszont igazán erőssé teszi, az a monádként való használhatósága. A funkcionális programozásból ismert kifejezést nem könnyű definiálni azok számára, akik még nem hallottak róla. Lényegében ha van bármennyi értékünk (0, 1 vagy több), és ha van egy függvényünk, ami átkonvertálja őket egy monád típusra, majd ez a típus tovább konvertálható más függvényekkel, akár van érték, akár nincs, akkor beszélhetünk monádról. Az Optional teljesíti a feltételeket, ugyanis az elsőre ott van az of vagy ofNullable metódusa, az utóbbira pedig a flatMap (erről később). Észrevehettük az orElse és az ifPresent példákban, hogy az Optional-t nem zavarja, ha üres, így is tud dolgozni az értékkel. Hasonló módon a Java 8-as Stream<T> is úgy működik, hogy ha nincs érték, akkor nem száll el hibával, szimplán csak nem konvertál semmit a map-pel, flatMap-pel, nem szűr a filter-rel, viszont a különböző lépéseken végigmegy. Effektíve a Stream egy monád 0 vagy akár végtelen értékkel, az Optional pedig szintén az, de csak 0 vagy 1 eleme lehet. Nem meglepő módon ennek is megvan a három említett metódus (map, flatMap, filter). Mindegyik úgy működik, hogy ha nem üres az Optional, akkor lefuttatja a kapott logikát, viszont ha üres, akkor rögtön visszatér önmagával. Előfordulhat, hogy adott kód lefutása után üres Optional-t kapunk vissza.

A map(…) metódus egy Function-t vár, amit az aktuális értékkel meghív és a kapott visszatérési értéket "becsomagolja" egy új Optional-ba. Ha az új érték null, akkor üres Optional-t kapunk.

A flatMap(…) hasonló, viszont olyan Function-t vár, ami egy Optional-lal tér vissza. Ha van érték, akkor meghívja a kapott függvényt és annak az eredményét kapjuk vissza. Tipikus esetben arra használhatjuk, hogy elkerüljük az egymásba ágyazott Optional példányokat.

A filter(…) arra jó, hogy kiszűrjük a "nem elég jó" értékeket. Lehet olyan helyzet, hogy ugyan nem null az objektum, viszont mégsem használható. Tipikus példa erre az üres String. A filter egy Predicate-t vár, amit a meglévő értékkel meghív. Ha false a visszatérési érték, akkor üres Optional-t kapunk.

Néhány példa ezek használatára (mellékelve az User osztályt a könnyebb megérthetőségért):


	public static class User {

		public static final User ANONYMOUS = new User("Nowhere", "[email protected]", null);

		private String address;

		private String email;

		private String secondaryEmail;

		public static User anonymous() {
			return ANONYMOUS;
		}

		public User(String address, String email, String secondaryEmail) {
			this.address = Objects.requireNonNull(address);
			this.email = Objects.requireNonNull(email);
			this.secondaryEmail = secondaryEmail;
		}

		public String getAddress() {
			return address;
		}

		public String getEmail() {
			return email;
		}

		public Optional<String> getSecondaryEmail() {
			return Optional.ofNullable(secondaryEmail);
		}
	}

	public void monadExamples() {
		final String email = "[email protected]";

		// Írjuk ki a felhasználó címét, ha létezik
		getUserByEmail(email)
			.map(User::getAddress)
			.ifPresent(System.out::println);

		// Lekérjük a másodlagos emailt, ha nincs, 'N/A' lesz itt
		getUserByEmail(email)
			.flatMap(User::getSecondaryEmail)
			.orElse("N/A");

		// Lekérjük a felhasználót, ha nincs vagy nem felel meg a feltételeknek
		// akkor hibát dobunk
		getUserByEmail(email)
			.filter(user -> user.getAddress().contains("Hungary"))
			.filter(user -> user.getSecondaryEmail().isPresent())
			.orElseThrow(IllegalStateException::new);
	}

Az Optional sok helyen használható. Ha nincs hozzáférésünk Java 8-as platformhoz, akkor használható a Guava könyvtárban található változat (a standard megvalósítás azon alapszik). Android platformot támogatja. Más nyelvekben is használható, létezik NodeJs csomagként elérhető Javascript port (ami undefined-ot és null-t is kezel).

Ha tetszett a cikk oszd meg másokkal is.