Porovnávání Optional

Při porovnávání Optional není nutné kontrolovat isPresent a volat hodnotu, ale jde rovnou porovnánat objekty.

Optional<Long> emptyLong = Optional.empty();
Optional<Long> l1 = Optional.of(1L);
Optional<String> s1 = Optional.of("abc");

System.out.println(emptyLong.equals(l1));
System.out.println(emptyLong.equals(Optional.empty()));
System.out.println(l1.equals(s1));
System.out.println(s1.equals(Optional.of("abc")));

Výsledek

false
true
false
true

Stejné to je v testech (např. při použití testNG):

assertNotEquals(emptyLong, l1);
assertEquals(emptyLong, Optional.empty());
assertNotEquals(l1, s1);
assertEquals(s1, Optional.of("abc"));

Takto asserty projdou. Pokud bychom to změnili např. na assertEquals(emptyLong, l1), dostali bychom následující chybu:

Exception in thread "main" java.lang.AssertionError: expected [Optional[1]] but found [Optional.empty]
	at org.testng.Assert.fail(Assert.java:93)
	at org.testng.Assert.failNotEquals(Assert.java:512)
	at org.testng.Assert.assertEqualsImpl(Assert.java:134)
	at org.testng.Assert.assertEquals(Assert.java:115)
	at org.testng.Assert.assertEquals(Assert.java:178)
	at ...

Java Optional efektivně

Klíčem k efektivnímu využití Optional je používání metod, které dokáží s Optional pracovat. Tyto metody buď zpracují hodnotu zabalenou v Optional, nebo, v případě, že není přítomna, vytvoří náhradní hodnotu (objekt).

Metoda Optional.ifPresent() jako parametr bere funkci java.util.function.Function a v případě, že Optional obsahuje hodnotu (referenci na objekt), je tato hodnota (reference) předána funkci. Pokud hodnotu neobsahuje, nic se nestane.

Stream<Integer> is1 = Stream.of(5, 7, 1, 8, 2);
Optional<Integer> max = is1.max(Integer::compareTo);
Stream<Integer> is2 = Stream.empty();
Optional<Integer> min = is2.min(Integer::compareTo);

Výpis výsledků do konzole (vypíše 8):

max.ifPresent((i) -> System.out.println(i));
min.ifPresent((i) -> System.out.println(i));

Přidání výsledků do listu:

List<Integer> results = new ArrayList<>();
max.ifPresent((i) -> results.add(i));
min.ifPresent((i) -> results.add(i));

Metoda Optional.ifPresent() zpracovává hodnotu v případě, že je přítomna. Dalším způsobem je použít metody, které vytvoří hodnotu (nebo něco provedou) v případě, že hodnota není přítomna. Pro tento případ se hodí metody optionalVariable.orElse(), optionalVariable.orElseGet(), optionalVariable.orElseThrow().

Stream<String> ss1 = Stream.of("lokomotiva", "auto", "letadlo", "raketa", "vzducholoď");
Optional<String> firstWithL = ss1.filter((word) -> word.startsWith("l")).findFirst();
Stream<String> ss2 = Stream.of("lokomotiva", "auto", "letadlo", "raketa", "vzducholoď");
Optional<String> anyWithQ = ss2.filter((word) -> word.startsWith("q")).findAny();

String withL = firstWithL.orElse("");
String withQ1 = anyWithQ.orElse("");

System.out.println(withL);
System.out.println(withQ1);

try {
	String withQ2 = anyWithQ.orElseThrow(Exception::new);
} catch (Exception e) {
	e.printStackTrace();
}

Výpis:

lokomotiva

java.lang.Exception
	at java.util.Optional.orElseThrow(...)

Metoda Optional.orElse() vrátí v případě, že Optional obsahuje null, náhradní hodnotu zadanou jako parametr. Pokud Optional obsahuje referenci, vrátí objekt. Metoda Optional.orElseThrow() vyhodí danou výjimku (pokud Optional obsahuje null).


Zdroj: HORSTMANN, Cay S. Java SE 8 for the really impatient. Upper Saddle River, NJ: Addison-Wesley, 2014, xv, 215 pages. ISBN 0321927761.

Proč používat Java Optional?

Není příliš velký rozdíl mezi:

Optional<T> op = ...;
op.get().someMethod();

vs

T val = ...;
val.someMethod();

Pokud se chceme vyhnout výjimce:

if (op.isPresent()) {
    T val = op.get();
    val.someMethod();
}

vs

if (val != null) {
    val.someMethod();
}

Pokud programátor Optional nepoužívá, ušetří dokonce pár znaků při psaní (nemusí volat get()). Proč tedy používat Optional?

Předchozí příspěvek na téma Optional se týkal třídy Optional z knihovny Guava. Tento příspěvek se věnuje třídě java.util.Optional, která je dostupná od Javy 8. Důvody pro jejich použití jsou ale obdobné. Guava Optional se používá v případě, že máte starší verzi Javy. V případě, že používáte Javu 8, knihovnu Guava nepotřebujete. 

Důvodem pro používání Optional je to, že null může mít více významů. Null může být prázdná hodnota, může představovat chybu, může to být též neinicializovaná proměnná. Pokud ale máme Optional<T> (Optional vlastně zabaluje hodnotu T), tak buď máme referenci na objekt typu T (Optional.isPresent() == true) a nebo referenci nemáme (Optional.isPresent() == false). V případě, že referenci nemáme, odpovídalo by to případu, kdy null vyjadřuje prázdnou hodnotu.

Další výhodou Optional je to, že bychom si při použití Optional měli uvědomit, že se jedná o hodnotu, která nepovinná (proto název optional) a podle toho se k ní chovat. U běžné proměnné nás nemusí napadnout, že je třeba provést kontrolu na to, zda třeba není null. To je důvod, proč některé metody vrací Optional. Dávají tím najevo, že nemusí vrátit žádnou hodnotu. Je to bezpečnější způsob práce než s null. Optional například vrací některé metody pracující s Stream (java.util.stream.Stream).

Stream<Integer> is1 = Stream.of(5, 7, 1, 8, 2);
Optional<Integer> max = is1.max(Integer::compareTo);

Stream<Integer> is2 = Stream.empty();
Optional<Integer> min = is2.min(Integer::compareTo);

Stream<String> ss1 = Stream.of("lokomotiva", "auto", "letadlo", "raketa", "vzducholoď");
Optional<String> firstWithL = ss1.filter((word) -> word.startsWith("l")).findFirst();

Stream<String> ss2 = Stream.of("lokomotiva", "auto", "letadlo", "raketa", "vzducholoď");
Optional<String> anyWithQ = ss2.filter((word) -> word.startsWith("q")).findAny();

Stream<String> ss3 = Stream.of("lokomotiva", "auto", "letadlo", "raketa", "vzducholoď");
Optional<String> shortest = ss3.min((word1, word2) -> word1.length() - word2.length());

Metody min(), max(), findAny(), findFirst() vrací Optional, protože Stream, který dostanou může být prázdný a tím pádem žádnou hodnotu nemusí najít (viz is2), nebo žádná taková hodnota nevyhovuje podmínkám (viz ss2.filter() na slova začínající na q -> žádné takové tam není).

Guava Optional.orNull()

V jednom z předchozích příspěvků jsem se věnoval třídě Optional z knihovny Guava. Nezmínil jsem ale užitečnou metodu orNull(). Tato metoda buď vrátí referenci na objekt v případě, že isPresent() je true, nebo null. Může se to hodit třeba v případě, kdy ve svém kódu používáte třídu Optional, ale jiné metody požadují objekt, nebo null.

Optional str1 = Optional.of("world");
Optional str2 = Optional.absent();

System.out.println(str1.orNull());
System.out.println(str2.orNull());

Výsledek.

world
null

V následujícím kódu si všimněte metod getString01() a getString02(). Obě dělají to samé, jen getString02() je mnohem kratší a přehlednějsí.

public static void main(String[] args) {
	Optional str1 = Optional.of("world");
	Optional str2 = Optional.absent();
	
	System.out.println(getString01(str1));
	System.out.println(getString01(str2));
	System.out.println(getString02(str1));
	System.out.println(getString02(str2));
}

private static String getString01(Optional str) {
	if (str.isPresent()) {
		return str.get();
	} else {
		return null;
	}
}

private static String getString02(Optional str) {
	return str.orNull();
}

Guava Optional.class

Null může znamenat mnoho věcí. Aby bylo jasnější, co vlastně má null představovat, a také proto, aby uživatel ošetřil stavy, kdy může dojít k NullPointerException je tady třída Optional z knihovny Guava. Tato třída je vhodná právě pro případy, kdy null má představovat prázdnou hodnotu.

Pokud proměnná obsahuje prázdnou hodnotu (například záznam nenalezen) použije se Optional.absent(). Pokud neprázdnou hodnotu pak je to Optional, který obsahuje referenci na danou hodnotu. Pokud by v tomto případě proměnná obsahovala null, znamenalo by to, že proměnná nebyla inicializována a byla by to chyba.

Tímto způsobem je přesně řečeno, kdy proměnná něco obsahuje a kdy ne. Dalším důvodem pro tuto třídu je přimět uživatele ke kontrole, a předejít případné NullPointerException.

Hodnotu (referenci na objekt) získlám z Optional voláním metody get(). Když už voláme get(), mělo by nás trknout, že hodnota může být absent a proto bychom měli nejdříve zavolat isPresent(). Pokud tato metoda vrátí true můžeme zavolat get() a s objektem dále pracovat. Určitou nevýhodou je neustálé volání get() metody.

Třída Person.

import com.google.common.base.Optional;

public class Person {
 
       private long id;
       private String name;
       private String surname;
       private Optional<String> nickname;
      
       public Person(long id, String name, String surname) {
             this.id = id;
             this.name = name;
             this.surname = surname;
             this.nickname = Optional.absent();
       }
       public Person(long id, String name, String surname, String nickname) {
             this.id = id;
             this.name = name;
             this.surname = surname;
             this.nickname = Optional.of(nickname);
       }
      
       public long getId() {
             return id;
       }
       public void setId(long id) {
             this.id = id;
       }
       public String getName() {
             return name;
       }
       public void setName(String name) {
             this.name = name;
       }
       public String getSurname() {
             return surname;
       }
       public void setSurname(String surname) {
             this.surname = surname;
       }
       public Optional<String> getNickname() {
             return nickname;
       }
       public void setNickname(Optional<String> nickname) {
             this.nickname = nickname;
       }
}

Třída Service s metodou, která vrací Optional.absent v případě, že daný objekt Person není v seznamu nalezen. V případě, že nalezen je, vrací Optional s referencí na nalezený objekt.

import java.util.List;

import com.google.common.base.Optional;

public class Service {

	public Optional<Person> findLongInList(List<Person> list, Long id) {
		for (Person person : list) {
			if (person.getId() == id) {
				return Optional.of(person);
			}
		}
		return Optional.absent();
	}
}

Třída App, ve které je ukázána práce s Optional.

import java.util.Arrays;
import java.util.List;

import com.google.common.base.Optional;
 
public class App {
       private static List<Person> persons = Arrays.asList(
                    new Person(1L, "František", "Koudelka"),
                    new Person(2L, "John", "Smith", "johny"),
                    new Person(3L, "James", "Bond"));
 
       public static void main(String[] args) {
             Service service = new Service();
             Optional<Person> personId2 = service.findLongInList(persons, 2L);
             Optional<Person> personId3 = service.findLongInList(persons, 3L);
             Optional<Person> personId4 = service.findLongInList(persons, 4L);
 
             // Pravujeme s objektem personId2
             // Zde většinu programátorů napadně kontrola na null.
             if (personId2.isPresent()) {
                    System.out.println("Jméno člověka s id 2: " + personId2.get().getName());
                    // Je ale jednoduché zapomenout na to, že getNickname může vrátit null. S Optional je to zřejmější.
                    if (personId2.get().getNickname().isPresent()) {
                           System.out.println("Jeho přezdívka je: " + personId2.get().getNickname().get());
                    }
             }
 
             // Pravujeme s objektem personId3
             if (personId3.isPresent()) {
                    System.out.println("Jméno člověka s id 3: " + personId3.get().getName());
                    if (personId3.get().getNickname().isPresent()) {
                           System.out.println("Jeho přezdívka je: " + personId3.get().getNickname().get());
                    }
             }
 
             // Pravujeme s objektem personId4
             if (personId4.isPresent()) {
                    System.out.println("Jméno člověka s id 4: " + personId4.get().getName());
                    if (personId4.get().getNickname().isPresent()) {
                           System.out.println("Jeho přezdívka je: " + personId4.get().getNickname().get());
                    }
             }
       }
}

Po spuštění dostaneme následující výsledek:

Jméno člověka s id 2: John
Jeho přezdívka je: johny
Jméno člověka s id 3: James

Zdroje: