講講Java8的Optional類

hireny發表於2021-01-13

前言

Java 8中引入了 Optional 類來解決 NullPointerException 與繁瑣的 null 檢查,該類首次出現在 Guava。Java 8 才成為類庫中的一部分。

入門

Optional 是一個封裝值的類,用於儲存型別為 T 的值;本質上,Optional 就是一個容器。

舉例來說,一個人可能有車也可能沒有,那麼 Person 類內部 car 變數就不應該宣告為 Car,當變數存在時,Optional 類只是對 Car 的簡單封裝。變數不存在時,會使用 Optional.empty() 方法返回空的 Optional 物件。如下所示:

但是 null 引用和 Optional.empty() 有什麼本質區別?從語義上,它們可以當成一回事兒,但實際上差別非常大:如果嘗試解引用一個 null,一定會觸發 NullPointerException,不過使用 Optional.empty() 是一個有效的物件。

下面我們來看一下 Optional 提供的功能。

建立

說到 Optional 的功能,我們首先要了解 Optional 例項的建立。

空Optional

正如前文提到,你可以通過靜態工廠方法 Optional.empty,建立一個空的 Optional 物件:

Optional<Car> option = Optional.empty();

因為 empty() 本身代表的就是空物件,所以呼叫 get 方法會丟擲 NoSuchElementException 異常。

非空Optional

你還可以使用靜態工廠方法 Optional.of ,依據一個非空值建立一個 Optional 物件:

Optional<Car> optional = Optional.of(car);

如果 car 是一個 null,這段程式碼會立即丟擲一個 NullPointerException,而不是等到你試圖訪問 car 的屬性值時才返回一個錯誤。

可為null的Optional

最後,使用靜態工廠方法 Optional.ofNullable,你可以建立一個允許 null 值的 Optional 物件:

Optional<Car> optional = Optional.ofNullable(car);

如果 carnull,那麼得到的 Optional 物件就是個空物件。我們可以檢視一下它的實現原理:

public static <T> Optional<T> ofNullable(T value) {
	return value == null ? empty() : of(value);
}

根據它的實現方式,我們可知,傳入的值是空值時,會返回 Optional.empty() 空物件。這有利於我們封裝那些可能為 null 的值。例如,有一個 Map<String, Object> 例項,訪問 key 索引時,如果沒有與 key 關聯的值,則會返回一個 null。因此,我們可以使用 Optional.ofNullable 方法封裝返回值:

Optional<Object> value = Optional.ofNullable(map.get("key"));
// 可以由上程式碼代替 Object value = map.get("key");

這樣可以將潛在的 null 隱患替換為空的 Optional 物件。

操作

我們建立了 Optional 例項後,需要對該例項進行操作。

isPresent & get

Optional 類中,isPresent 方法對 Optional 例項進行判斷,是否包含值,如果存在值,就返回 true,否則返回 false;與之相對的是 isEmpty 方法Optional 類中還有 get 方法,它是用來獲取 Optional 例項中的值。

Optional<String> optional = Optional.of("is present");
if (optional.isPresent()) {
	System.out.println("the value is " + optional.get());
}

isPresentget 一般組合使用來避免 NullPointerException

public boolean isPresent() {
	return value != null;
}
public T get() {
	if (value == null) {
		throw new NoSuchElementException("No value present");
	}
	return value;
}

從原始碼中可看出,get 方法在取值時,要進行判空操作,如果不使用 isPresent 方法,可能會出現空指標異常。但是這種方式和在程式碼中if(null != value) 沒有區別,因此我們要儘量避免使用該組合。

ifPresent

除了 isPresent 的簡潔方法,Optional 還提供了接收函式式引數的介面 ifPresent

public void ifPresent(Consumer<? super T> action) {
	if (value != null) {
		action.accept(value);
	}
}

該方法會接收一個消費型函式。如果 Optional 例項中的值不為空,則呼叫 Consumeraccept 方法對 value 進行消費,若為空則不做處理。上面的例子可以使用 ifPresent 重寫:

Optional<String> optional = Optional.of("is present");
optional.isPresent((val) -> System.out.println("the value is " + val));

orElse

我們還可以使用 orElse 方法讀取 Optional 中的值。

public T orElse(T other) {
	return value != null ? value : other;
}

使用這種方式可以定義一個預設值,這種方式當遭遇 Optional 中的變數為空時,預設值會作為該方法的返回值。

String optGet = null;
String orElse = Optional.ofNullable(optGet).orElse("Default");
/**
 * 輸出結果:
 * Default
 */

orElseGet

如果該方法不夠,我們可以使用另一種方式 orElseGet

public T orElseGet(Supplier<? extends T> supplier) {
	return value != null ? value : supplier.get();
}

該方法與 orElse 的區別就是值不存在時,呼叫實現 Supplier 介面的方法或Lambda表示式來返回預設值。

String optGet = null;
String orElse = Optional.ofNullable(optGet).orElse(() -> "Default");
/**
 * 輸出結果:
 * Default
 */

orElseThrow

orElseThrow方法是在有值時返回其值,無值的時候會丟擲由 Supplier 建立的異常。我們看一下它的實現原理:

public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
	if (value != null) {
		return value;
	} else {
		throw exceptionSupplier.get();
	}
}

orElseThrow 原理中,會傳入一個Lambda表示式或方法,如果值不存在來丟擲異常:

class NoValueException extends RuntimeException {
    public NoValueException() {
        super();
    }
    @Override
    public String getMessage() {
        return "No value present in the Optional instance";
    }
}
public static Integer orElseThrow() {
	return (Integer) Optional.empty().orElseThrow(NoValueException::new);
}
public static void main(String[] args) {
	orElseThrow();
}
/**
 * 控制檯會丟擲異常:
 * Exception in thread "main" xx.NoValueException: No value present in the Optional instance
 */

orElseThroworElseGet 的區別就是一個在無值的時候丟擲異常,一個在無值的時候使用Lambda表示式來實現預設值。

orElseThrow 只是在無值的時候丟擲異常,那本身會丟擲異常的方法呢?

現在,我們拿 Integer.parseInt(String) 做個例子:

public static Integer toInt(String s) {
	try {
		// 如果String能轉換為對應的Integer,將其封裝在Optional物件中返回
		return Integer.parseInt(s);
	} catch (NumberFormatException e) {
		return null;	// 返回null 或者丟擲異常
	}
}

在將 String 轉換為 int 時,如果無法解析到對應的整型,該方法會丟擲 NumberFormatException 異常。我們在該方法中使用 try/catch 語句捕獲了該異常,不能使用 if 條件判斷來控制一個變數的值是否為空。

這時,我們可以使用 Optional 類,來對無法轉換的 String 時返回的非法值進行建模,因此,我們可以對上述方法進行改進:

public static Optional<Integer> toInt(String s) {
	try {
		// 如果String能轉換為對應的Integer,將其封裝在Optional物件中返回
		return Optional.of(Integer.parseInt(s));
	} catch (NumberFormatException e) {
		return Optional.empty();	// 否則返回一個空的 Optional 物件
	}
}

這種返回 Optional 的方式適用很多方法,我們只需要獲取被 Optional 包裝的值的例項即可。

map

Optional 提供了 map 方法用於從物件中提取資訊,它的工作原理如下:

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
	Objects.requireNonNull(mapper);
	if (!isPresent())
		return empty();
	else {
		return Optional.ofNullable(mapper.apply(value));
	}
}

map 操作會將提供的函式應用於流的每個元素。我們可以把 Optional 物件看成一種特殊的集合資料,它至多包含一個元素。如果 Optional 包含一個值,那通過實現了 Function 介面的 Lambda 表示式對值進行轉換。如果不熟悉 Function 介面,可以參考這篇文章。map 方法示例如下:

class Car {
	private String name;
	private String type;
	...省略getter與setter...
}
Optional<Car> optional = Optional.ofNullable(car);
Optional<String> name = optional.map(Car::getName);

flatMap

我們可以使用 map 方法來從被 Optional 類包裝的 Person 類中獲取 Car 的名稱:

class Person {
	private Optional<Car> car;
	public Person(Car car) {
		this.car = Optional.of(car);
	}
	...省略getter與setter...
}
Person person = new Person(new Car());
Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.map(Person::getCar)
	.map(Car::getName);

不幸的是,這段程式碼無法通過編譯。為什麼呢?optPersonOptional<Person> 型別的變數,呼叫 map 方法應該沒有問題。但 getCar 返回的是一個 Optional<Car> 型別的物件,這意味著 map 操作的結果是一個 Optional<Optional<Car>> 型別的物件。因此,它對 getName 的呼叫是非法的,因為最外層的 optional 物件包含了另一個 optional 物件的值,而它當然不會支援 getName 方法。

所以,我們使用 flatMap 方法。該方法接受一個函式作為引數,這個函式的返回值是另一個流。

public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
	Objects.requireNonNull(mapper);
	if (!isPresent()) {
		return empty();
	} else {
		@SuppressWarnings("unchecked")
		Optional<U> r = (Optional<U>) mapper.apply(value);
		return Objects.requireNonNull(r);
	}
}

參照 map 函式,使用 flatMap 重寫上述的示例:

Optional<String> name = optPerson.flatMap(Person::getCar).map(Car::getName);

filter

有時候我們需要對 Optional 中的值進行過濾,獲得我們需要的結果,我們就可以使用 filter 方法:

public Optional<T> filter(Predicate<? super T> predicate) {
	Objects.requireNonNull(predicate);
	if (!isPresent())
		return this;
	else
		return predicate.test(value) ? this : empty();
}

該方法接受 Predicate 謂詞作為引數。如果 Optional 物件的值存在,並且符合謂詞的條件,即操作結果為truefilter 方法不做任何改變並返回其值;否則就將該值過濾掉並返回一個空的 Optional 物件。

Optional<String> optionalS = Optional.of("13846901234");
optionalS = optionalS.filter(s -> s.contains("138"));
/**
 * 上述 `filter` 方法滿足條件可以返回同一個Optional,否則返回空Optional
 */

總結

Java 8引入的 java.util.Optional<T> 讓我們以函數語言程式設計的方式處理 null,防止空指標異常;並支援多種方式用於操作值,比如:mapflatMapfilter,這樣可以拋棄巢狀的 if-else 程式碼塊,設計更好的 API,程式碼的可讀性也大大提高,但是如果在域模型中使用 Optional,由於沒有實現 Serializable 介面,不能進行例項化,也不能作為類的欄位。

更多內容請關注公眾號「海人的部落格」,回覆「資源」即可獲得免費學習資源!

相關文章