《Java8實戰》筆記(10):用Optional取代null

用Optional取代null

本文的代碼

衆所周知,對任何一位Java程序員來說,無論是初出茅廬的素人,還是久經江湖的老司機,NullPointerException都是他心中的痛,可是我們又無能爲力,因爲這就是我們爲了使用方便甚至不可避免的像null引用這樣的構造所付出的代價。這就是程序設計世界裏大家都持有的觀點,然而,這可能並非事實的全部真相,只是我們根深蒂固的一種偏見。

1965年,英國一位名爲Tony Hoare的計算機科學家在設計ALGOL W語言時提出了null引用的想法。

如何爲缺失的值建模

假設你需要處理下面這樣的嵌套對象,這是一個擁有汽車及汽車保險的客戶。

public class Person {
	private Car car;

	public Car getCar() {
		return car;
	}
}

public class Car {
	private Insurance insurance;

	public Insurance getInsurance() {
		return insurance;
	}
}

public class Insurance {
	private String name;

	public String getName() {
		return name;
	}
}

public String getCarInsuranceName(Person person) {
	return person.getCar().getInsurance().getName();
}

這段代碼看起來相當正常,但是現實生活中很多人沒有車。所以調用getCar方法的結果會怎樣呢?在實踐中,一種比較常見的做法是返回一個null引用,表示該值的缺失,即用戶沒有車。

而接下來,對getInsurance的調用會返回null引用的insurance,這會導致運行時出現一個NullPointerException,終止程序的運行。但這還不是全部。如果返回的person值爲null會怎樣?如果getInsurance的返回值也是null,結果又會怎樣?

採用防禦式檢查減少NullPointerException

怎樣做才能避免這種不期而至的NullPointerException呢?通常,你可以在需要的地方添加null的檢查(過於激進的防禦式檢查甚至會在不太需要的地方添加檢測代碼),並且添加的方式往往各有不同。

深層質疑

public String getCarInsuranceName(Person person) {
	if (person != null) {
		Car car = person.getCar();
		if (car != null) {
			Insurance insurance = car.getInsurance();
			if (insurance != null) {
				return insurance.getName();
			}
		}
	}
	return "Unknown";
}

標記爲“深層質疑”,原因是它不斷重複着一種模式:每次你不確定一個變量是否爲null時,都需要添加一個進一步嵌套的if塊,也增加了代碼縮進的層數。很明顯,這種方式不具備擴展性,同時還犧牲了代碼的可讀性

過多的退出語句

public String getCarInsuranceName(Person person) {
	if (person == null) {
		return "Unknown";
	}
	Car car = person.getCar();
	if (car == null) {
		return "Unknown";
	}
	Insurance insurance = car.getInsurance();
	if (insurance == null) {
		return "Unknown";
	}
	return insurance.getName();
}

你試圖避免深層遞歸的if語句塊,採用了一種不同的策略:每次你遭遇null變量,都返回一個字符串常量“Unknown”。然而,這種方案遠非理想,現在這個方法有了四個截然不同的退出點,使得代碼的維護異常艱難。更糟的是,發生null時返回的默認值,即字符串“Unknown”在三個不同的地方重複出現——出現拼寫錯誤的概率不小!當然,你可能會說,我們可以用把它們抽取到一個常量中的方式避免這種問題。

進一步而言,這種流程是極易出錯的;如果你忘記檢查了那個可能爲null的屬性會怎樣?你會了解使用null來表示變量值的缺失是大錯特錯的。

null帶來的種種問題

在Java程序開發中使用null會帶來理論和實際操作上的種種問題

  • 它是錯誤之源 。NullPointerException是目前Java程序開發中最典型的異常。
  • 它會使你的代碼膨脹。它讓你的代碼充斥着深度嵌套的null檢查,代碼的可讀性糟糕透頂。
  • 它自身是毫無意義的。null自身沒有任何的語義,尤其是,它代表的是在靜態類型語言中以一種錯誤的方式對 缺失變量值的建模。
  • 它破壞了Java的哲學。Java一直試圖避免讓程序員意識到指針的存在,唯一的例外是:null指針。
  • 它在Java的類型系統上開了個口子。null並不屬於任何類型,這意味着它可以被賦值給任意引用類型的變量。這會導致問題,原因是當這個變量被傳遞到系統中的另一個部分後,你將無法獲知這個null變量最初的賦值到底是什麼類型。

其他語言中null的替代品

比如Groovy,通過引入安全導航操作符(Safe Navigation Operator,標記爲?)可以安全訪問可能爲null的變量。

def carInsuranceName = person?.car?.insurance?.name

幾乎所有的Java程序員碰到NullPointerException時的第一衝動就是添加一個if語句,在調用方法使用該變量之前檢查它的值是否爲null,快速地搞定問題。如果你按照這種方式解決問題,絲毫不考慮你的算法或者你的數據模型在這種狀況下是否應該返回一個null,那麼你其實並沒有真正解決這個問題,只是暫時地掩蓋了問題,使得下次該問題的調查和修復更加困難,而你很可能就是下個星期或下個月要面對這個問題的人。剛纔的那種方式實際上是掩耳盜鈴,只是在清掃地毯下的灰塵。

而Groovy的null安全解引用操作符也只是一個更強大的掃把,讓我們可以毫無顧忌地犯錯。你不會忘記做這樣的檢查,因爲類型系統會強制你進行這樣的操作。


另一些函數式語言,比如Haskell、Scala,試圖從另一個角度處理這個問題。Haskell中包含了一個Maybe類型,它本質上是對optional值的封裝。Maybe類型的變量可以是指定類型的值,也可以什麼都不是。

但是它並沒有null引用的概念。Scala有類似的數據結構,名字叫Option[T],它既可以包含類型爲T的變量,也可以不包含該變量要使用這種類型,你必須顯式地調用Option類型的available操作,檢查該變量是否有值,而這其實也是一種變相的“null檢查”。

Optional類入門

汲取Haskell和Scala的靈感,Java 8中引入了一個新的類java.util.Optional<T>。這是一個封裝Optional值的類。舉例來說,使用新的類意味着,如果你知道一個人可能有也可能沒有車,那麼Person類內部的car變量就不應該聲明爲Car,遭遇某人沒有車時把null引用賦值給它,而是應該直接將其聲明爲Optional<Car>類型。

變量存在時,Optional類只是對類簡單封裝。變量不存在時,缺失的值會被建模成一個“空”的Optional對象,由方法Optional.empty()返回。Optional.empty()方法是一個靜態工廠方法,它返回Optional類的特定單一實例。

你可能還有疑惑,null引用和Optional.empty()有什麼本質的區別嗎?從語義上,你可以把它們當作一回事兒,但是實際中它們之間的差別非常大: 如果你嘗試解引用一個null , 一定會觸發NullPointerException , 不過使用Optional.empty()就完全沒事兒,它是Optional類的一個有效對象,多種場景都能調用,

使用Optional而不是null的一個非常重要而又實際的語義區別是,第一個例子中,我們在聲明變量時使用的是Optional<Car>類型,而不是Car類型,這句聲明非常清楚地表明瞭這裏發生變量缺失是允許的。與此相反,使用Car這樣的類型,可能將變量賦值爲null,這意味着你需要獨立面對這些,你只能依賴你對業務模型的理解,判斷一個null是否屬於該變量的有效範疇。


使用Optional類對最初的代碼進行重構

public class Person {
	private Optional<Car> car;

	public Optional<Car> getCar() {
		return car;
	}
}

public class Car {
	private Optional<Insurance> insurance;

	public Optional<Insurance> getInsurance() {
		return insurance;
	}
}

public class Insurance {
	private String name;

	public String getName() {
		return name;
	}
}

在你的代碼中始終如一地使用Optional,能非常清晰地界定出變量值的缺失是結構上的問題,還是你算法上的缺陷,抑或是你數據中的問題。另外,我們還想特別強調,引入Optional類的意圖並非要消除每一個null引用。與此相反,它的目標是幫助你更好地設計出普適的API,讓程序員看到方法簽名,就能瞭解它是否接受一個Optional的值。這種強制會讓你更積極地將變量從Optional中解包出來,直面缺失的變量值。

應用Optional的幾種模式

創建Optional對象

聲明一個空的Optional

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

依據一個非空值創建Optional

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

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

可接受null的Optional

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

使用map從Optional對象中提取和轉換值

從對象中提取信息是一種比較常見的模式。比如,你可能想要從insurance公司對象中提取公司的名稱。提取名稱之前,你需要檢查insurance對象是否爲null

String name = null;
if(insurance != null){
	name = insurance.getName();
}

爲了支持這種模式,Optional提供了一個map方法

Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);

使用flatMap鏈接Optional對象

public String getCarInsuranceName(Person person) {
	return person.getCar().getInsurance().getName();
}

你的第一反應可能是我們可以利用map重寫之前的代碼,

Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.map(Person::getCar)
				.map(Car::getInsurance)
				.map(Insurance::getName);

不幸的是,這段代碼無法通過編譯。

optPerson是Optional<Person>類型的變量, 調用map方法應該沒有問題。但getCar返回的是一個Optional<Car>類型的對象,這意味着map操作的結果是一個Optional<Optional<Car>>類型的對象。

因此,它對getInsurance的調用是非法的,因爲最外層的optional對象包含了另一個optional對象的值,而它當然不會支持getInsurance方法。

flatMap方法解決這個問題。

使用流時,flatMap方法接受一個函數作爲參數,這個函數的返回值是另一個流。這個方法會應用到流中的每一個元素,最終形成一個新的流的流。但是flagMap會用流的內容替換每個新生成的流。

換句話說,由方法生成的各個流會被合併或者扁平化爲一個單一的流。這裏你希望的結果其實也是類似的,但是你想要的是將兩層的optional合併爲一個。

這個例子中,傳遞給流的flatMap方法會將每個正方形轉換爲另一個流中的兩個三角形。那麼,map操作的結果就包含有三個新的流,每一個流包含兩個三角形,但flatMap方法會將這種兩層的流合併爲一個包含六個三角形的單一流。

類似地,傳遞給optional的flatMap方法的函數會將原始包含正方形的optional對象轉換爲包含三角形的optional對象。如果將該方法傳遞給map方法,結果會是一個Optional對象,而這個Optional對象中包含了三角形;但flatMap方法會將這種兩層的Optional對象轉換爲包含三角形的單一Optional對象。

使用Optional獲取car的保險公司名稱

public String getCarInsuranceName(Optional<Person> person) {
	return person.flatMap(Person::getCar)
		.flatMap(Car::getInsurance)
		.map(Insurance::getName)
		.orElse("Unknown");
}

通過比較之前的兩個代碼清單,我們可以看到,處理潛在可能缺失的值時,使用Optional具有明顯的優勢。這一次,你可以用非常容易卻又普適的方法實現之前你期望的效果——不再需要使用那麼多的條件分支,也不會增加代碼的複雜性。

再一次看到這種方式的優點,它通過類型系統讓你的域模型中隱藏的知識顯式地體現在你的代碼中,換句話說,你永遠都不應該忘記語言的首要功能就是溝通,即使對程序設計語言而言也沒有什麼不同。聲明方法接受一個Optional參數,或者將結果作爲Optional類型返回,讓你的同事或者未來你方法的使用者,很清楚地知道它可以接受空值,或者它可能返回一個空值。

使用Optional解引用串接的Person/Car/Insurance對象

由Optional<Person>對象,我們可以結合使用之前介紹的map和flatMap方法,從Person中解引用出Car,從Car中解引用出Insurance,從Insurance對象中解引用出包含insurance公司名稱的字符串。

在域模型中使用Optional,以及爲什麼它們無法序列化

上面展示瞭如何在你的域模型中使用Optional,將允許缺失或者暫 無定義的變量值用特殊的形式標記出來。然而,Optional類設計者的初衷並非如此,他們構思時懷揣的是另一個用例。這一點,Java語言的架構師Brian Goetz曾經非常明確地陳述過,Optional的設計初衷僅僅是要支持能返回Optional對象的語法。

由於Optional類設計時就沒特別考慮將其作爲類的字段使用,所以它也並未實現Serializable接口。由於這個原因,如果你的應用使用了某些要求序列化的庫或者框架,在域模型中使用Optional,有可能引發應用程序故障

然而,通過前面的介紹,你已經看到用Optional聲明域模型中的某些類型是個不錯的主意,尤其是你需要遍歷有可能全部或部分爲空,或者可能不存在的對象時。如果你一定要實現序列化的域模型,作爲替代方案,我們建議你像下面這個例子那樣,提供一個能訪問聲明爲Optional、變量值可能缺失的接口,代碼清單如下:

public class Person {
	private Car car;
	public Optional<Car> getCarAsOptional() {
		return Optional.ofNullable(car);
	}
}

默認行爲及解引用Optional對象

Optional類提供了多種方法讀取Optional實例中的變量值。

  • get()是這些方法中最簡單但又最不安全的方法。如果變量存在,它直接返回封裝的變量值,否則就拋出一個NoSuchElementException異常。所以,除非你非常確定Optional變量一定包含值,否則使用這個方法是個相當糟糕的主意。此外,這種方式即便相對於嵌套式的null檢查,也並未體現出多大的改進。

  • orElse(T other)是我們在代碼清單10-5中使用的方法,正如之前提到的,它允許你在Optional對象不包含值時提供一個默認值。

  • orElseGet(Supplier<? extends T> other)是orElse方法的延遲調用版,Supplier方法只有在Optional對象不含值時才執行調用。如果創建默認值是件耗時費力的工作,你應該考慮採用這種方式(藉此提升程序的性能),或者你需要非常確定某個方法僅在Optional爲空時才進行調用,也可以考慮該方式(這種情況有嚴格的限制條件)。

  • orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常類似,它們遭遇Optional對象爲空時都會拋出一個異常,但是使用orElseThrow你可以定製希望拋出的異常類型。

  • ifPresent(Consumer<? super T>)讓你能在變量值存在時執行一個作爲參數傳入的方法,否則就不進行任何操作。

兩個Optional對象的組合

假設你有這樣一個方法,它接受一個Person和一個Car對象,並以此爲條件對外部提供的服務進行查詢,通過一些複雜的業務邏輯,試圖找到滿足該組合的最便宜的保險公司:

public Insurance findCheapestInsurance(Person person, Car car) {
	// 不同的保險公司提供的查詢服務
	// 對比所有數據
	return cheapestCompany;
}

假設你想要該方法的一個null-安全的版本,它接受兩個Optional對象作爲參數,返回值是一個Optional<Insurance>對象,如果傳入的任何一個參數值爲空,它的返回值亦爲空

public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
	if (person.isPresent() && car.isPresent()) {
		return Optional.of(findCheapestInsurance(person.get(), car.get()));
	} else {
		return Optional.empty();
	}
}

以不解包的方式組合兩個Optional對象

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

使用filter剔除特定的值

經常需要調用某個對象的方法,查看它的某些屬性。比如,你可能需要檢查保險公司的名稱是否爲“Cambridge-Insurance”。爲了以一種安全的方式進行這些操作,你首先需要確定引用指向的Insurance對象是否爲null,之後再調用它的getName方法,

Insurance insurance = ...;

if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){
	System.out.println("ok");
}

使用Optional對象的filter方法,這段代碼可以重構如下:

Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName()))
	.ifPresent(x -> System.out.println("ok"));

對Optional對象進行過濾

找出年齡大於或者等於minAge參數的Person所對應的保險公司列表。

public String getCarInsuranceName(Optional<Person> person, int minAge) {
	return person.filter(p -> p.getAge() >= minAge)
				.flatMap(Person::getCar)
				.flatMap(Car::getInsurance)
				.map(Insurance::getName)
				.orElse("Unknown");
}

Optional類的方法

方法 描述
empty 返回一個空的Optional實例
filter 如果值存在並且滿足提供的謂詞,就返回包含該值的Optional對象;否則返回一個空的Optional對象
flatMap 如果值存在,就對該值執行提供的mapping函數調用,返回一個Optional類型的值,否則就返回一個空的Optional對象
get 如果該值存在,將該值用Optional封裝返回,否則拋出一個NoSuchElementException異常
ifPresent 如果值存在,就執行使用該值的方法調用,否則什麼也不做
isPresent 如果值存在就返回true,否則返回false
map 如果值存在,就對該值執行提供的mapping函數調用
of 將指定值用Optional封裝之後返回,如果該值爲null,則拋出一個NullPointerException異常
ofNullable 將指定值用Optional封裝之後返回,如果該值爲null,則返回一個空的Optional對象
orElse 如果有值則將其返回,否則返回一個默認值
orElseGet 如果有值則將其返回,否則返回一個由指定的Supplier接口生成的值
orElseThrow 如果有值則將其返回,否則拋出一個由指定的Supplier接口生成的異常

使用Optional的實戰示例

有效地使用Optional類意味着你需要對如何處理潛在缺失值進行全面的反思。這種反思不僅僅限於你曾經寫過的代碼,更重要的可能是,你如何與原生Java API實現共存共贏。

用Optional封裝可能爲null的值

假設你有一個Map<String, Object>方法,訪問由key索引的值時,如果map中沒有與key關聯的值,該次調用就會返回一個null

Object value = map.get("key");

使用Optional封裝map的返回值,你可以對這段代碼進行優化。要達到這個目的有兩種方式:你可以使用笨拙的if-then-else判斷語句,毫無疑問這種方式會增加代碼的複雜度;或者你可以採用我們前文介紹的Optional.ofNullable方法:

Optional<Object> value = Optional.ofNullable(map.get("key"));

異常與Optional的對比

由於某種原因,函數無法返回某個值,這時除了返回null,Java API比較常見的替代做法是拋出一個異常。

這種情況比較典型的例子是使用靜態方法Integer.parseInt(String),將 String轉換爲int。在這個例子中,如果String無法解析到對應的整型,該方法就拋出一個NumberFormatException。最後的效果是,發生String無法轉換爲int時,代碼發出一個遭遇非法參數的信號,唯一的不同是,這次你需要使用try/catch 語句,而不是使用if條件判斷來控制一個變量的值是否非空。

你也可以用空的Optional對象,對遭遇無法轉換的String時返回的非法值進行建模,這時你期望parseInt的返回值是一個optional。我們無法修改最初的Java方法,但是這無礙我們進行需要的改進,你可以實現一個工具方法,將這部分邏輯封裝於其中,最終返回一個我們希望的Optional對象

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

強烈建議是,你可以將多個類似的方法封裝到一個工具類中,讓我們稱之爲OptionalUtility。通過這種方式,你以後就能直接調用OptionalUtility.stringToInt方法,將String轉換爲一個Optional<Integer>對象,而不再需要記得你在其中封裝了笨拙的try/catch的邏輯了。

基礎類型的Optional對象,以及爲什麼應該避免使用它們

不知道你注意到了沒有, 與Stream 對象一樣, Optional 也提供了類似的基礎類型——OptionalInt、OptionalLong以及OptionalDouble——所以代碼可以不返回Optional<Integer>,而是直接返回一個OptionalInt類型的對象。

前面討論過使用基礎類型Stream的場景,尤其是如果Stream對象包含了大量元素,出於性能的考量,使用基礎類型是不錯的選擇,但對Optional對象而言,這個理由就不成立了,因爲Optional對象最多隻包含一個值。

不推薦大家使用基礎類型的Optional,因爲基礎類型的Optional不支持map、flatMap以及filter方法,而這些卻是Optional類最有用的方法。

此外,與Stream一樣,Optional對象無法由基礎類型的Optional組合構成

把之前所有內容整合起來

假設你需要向你的程序傳遞一些屬性。爲了舉例以及測試你開發的代碼,你創建了一些示例屬性,如下所示:

Properties props = new Properties();
props.setProperty("a", "5");
props.setProperty("b", "true");
props.setProperty("c", "-3");

假設你的程序需要從這些屬性中讀取一個值,該值是以秒爲單位計量的一段時間。由於一段時間必須是正數,你想要該方法符合下面的簽名:

public int readDuration(Properties props, String name)

即,如果給定屬性對應的值是一個代表正整數的字符串,就返回該整數值,任何其他的情況都返回0。

public static int readDurationImperative(Properties props, String name) {
	String value = props.getProperty(name);
	if (value != null) {
		try {
			int i = Integer.parseInt(value);
			if (i > 0) {
				return i;
			}
		} catch (NumberFormatException nfe) {
		}
	}
	return 0;
}

使用Optional從屬性中讀取duration

public int readDuration(Properties props, String name) {
	return Optional.ofNullable(props.getProperty(name))
			.flatMap(OptionalUtility::stringToInt)
			.filter(i -> i > 0)
			.orElse(0);
}

小結

  • null引用在歷史上被引入到程序設計語言中,目的是爲了表示變量值的缺失。
  • Java 8中引入了一個新的類java.util.Optional<T>,對存在或缺失的變量值進行建模。
  • 你可以使用靜態工廠方法Optional.empty、Optional.of以及Optional.ofNullable創建Optional對象。
  • Optional類支持多種方法,比如map、flatMap、filter,它們在概念上與Stream類中對應的方法十分相似。
  • 使用Optional會迫使你更積極地解引用Optional對象,以應對變量值缺失的問題,最終,你能更有效地防止代碼中出現不期而至的空指針異常。
  • 使用Optional能幫助你設計更好的API,用戶只需要閱讀方法簽名,就能瞭解該方法是否接受一個Optional類型的值。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章