1. 程式人生 > >Java開發筆記(七十七)使用Optional規避空指針異常

Java開發筆記(七十七)使用Optional規避空指針異常

代碼優化 解決問題 https tar 是否 思路 element 代碼示例 技術

前面在介紹清單用法的時候,講到了既能使用for循環遍歷清單,也能通過stream流式加工清單。譬如從一個蘋果清單中挑選出紅蘋果清單,采取for循環和流式處理都可以實現。下面是通過for循環挑出紅蘋果清單的代碼例子:

	// 通過簡單的for循環挑出紅蘋果清單
	private static void getRedAppleWithFor(List<Apple> list) {
		List<Apple> redAppleList = new ArrayList<Apple>();
		for (Apple apple : list) { // 遍歷現有的蘋果清單
			if (apple.isRedApple()) { // 判斷是否為紅蘋果
				redAppleList.add(apple);
			}
		}
		System.out.println("for循環 紅蘋果清單=" + redAppleList.toString());
	}

至於通過流式處理挑出紅蘋果清單的代碼示例如下:

	// 通過流式處理挑出紅蘋果清單
	private static void getRedAppleWithStream(List<Apple> list) {
		// 挑出紅蘋果清單
		List<Apple> redAppleList = list.stream() // 串行處理
				.filter(Apple::isRedApple) // 過濾條件。專門挑選紅蘋果
				.collect(Collectors.toList()); // 返回一串清單
		System.out.println("流式處理 紅蘋果清單=" + redAppleList.toString());
	}

然而上述的兩段代碼只能在數據完整的情況下運行,一旦原始的蘋果清單存在數據缺失,則兩段代碼均無法正常運行。例如,蘋果清單為空,清單中的某條蘋果記錄為空,某個蘋果記錄的顏色字段為空,這三種情況都會導致程序遇到空指針異常而退出。看來編碼不是一件輕松的活,不但要讓程序能跑通正確的數據,而且要讓程序對各種非法數據應對自如。換句話說,程序要足夠健壯,要擁有適當的容錯性,即使是吃錯藥了,也要能夠自動吐出來,而不是硬吞下去結果一病不起。對應到挑選紅蘋果的場合中,則需層層遞進判斷原始蘋果清單的數據完整性,倘若發現任何一處的數據存在缺漏情況(如出現空指針),就跳過該處的數據處理。於是在for循環前後添加了空指針校驗的紅蘋果挑選代碼變成了下面這樣:

	// 在for循環的內外添加必要的空指針校驗
	private static void getRedAppleWithNull(List<Apple> list) {
		List<Apple> redAppleList = new ArrayList<Apple>();
		if (list != null) { // 判斷清單非空
			for (Apple item : list) { // 遍歷現有的蘋果清單
				if (item != null) { // 判斷該記錄非空
					if (item.getColor() != null) { // 判斷顏色字段非空
						if (item.isRedApple()) { // 判斷是否為紅蘋果
							redAppleList.add(item);
						}
					}
				}
			}
		}
		System.out.println("加空指針判斷 紅蘋果清單=" + redAppleList.toString());
	}

由此可見修改後的for循環代碼一共增加了三個空指針判斷,但是上面代碼明顯太復雜了,不必說層層嵌套的條件分支,也不必說多次縮進的代碼格式,單單說後半部分的數個右花括號,簡直叫人看得眼花繚亂,難以分清哪個右花括號究竟對應上面的哪個流程控制語句。這種情況實在考驗程序員的眼力,要是一不留神看走眼放錯其它代碼的位置,豈不是撿了芝麻丟了西瓜?
空指針的校驗代碼固然繁瑣,卻是萬萬少不了的,究其根源,乃是Java設計之初偷懶所致。正常情況下,聲明某個對象時理應為其分配默認值,從而確保該對象在任何時候都是有值的,但早期的Java圖省事,如果程序員沒在聲明對象的同時加以賦值,那麽系統也不給它初始化,結果該對象只好指向一個虛無縹緲的空間,而在太虛幻境中無論做什麽事情都只能是黃粱一夢。
空指針的設計缺陷根深蒂固,以至於後來的Java版本難以根除該毛病,遲至Java8才推出了針對空指針的解決方案——可選器Optional。Optional本質上是一種特殊的容器,其內部有且僅有一個元素,同時該元素還可能為空。圍繞著這個可空元素,Optional衍生出若幹泛型方法,目的是將復雜的流程控制語句歸納為接續進行的方法調用。為了兼容已有的Java代碼,通常並不直接構造Optional實例,而是調用它的ofNullable方法塞入某個實體對象,再調用Optional實例的其它方法進行處理。Optional常用的實例方法羅列如下:
get:獲取可選器中保存的元素。如果元素為空,則扔出無此元素異常NoSuchElementException。
isPresent:判斷可選器中元素是否為空。非空返回true,為空返回false。
ifPresent:如果元素非空,則對該元素執行指定的Consumer消費事件。
filter:如果元素非空,則根據Predicate斷言條件檢查該元素是否符合要求,只有符合才原樣返回,若不符合則返回空值。
map:如果元素非空,則執行Function函數實例規定的操作,並返回指定格式的數據。
orElse:如果元素非空就返回該元素,否則返回指定的對象值。
orElseThrow:如果元素非空就返回該元素,否則扔出指定的異常。
接下來看一個Optional的簡單應用例子,之前在蘋果類中寫了isRedApple方法,用來判斷自身是否為紅蘋果,該方法的代碼如下所示:

	// 判斷是否紅蘋果
	public boolean isRedApple() {
		// 不嚴謹的寫法。一旦color字段為空,就會發生空指針異常
		return this.color.toLowerCase().equals("red");
	}

顯而易見這個isRedApple方法很不嚴謹,一旦顏色color字段為空,就會發生空指針異常。常規的補救自然是增加空指針判斷,遇到空指針的情況便自動返回false,此時方法代碼優化如下:

	// 判斷是否紅蘋果
	public boolean isRedApple() {
		// 常規的寫法,判斷color字段是否為空,再做分支處理
		boolean isRed = (this.color==null) ? false : this.color.toLowerCase().equals("red");
		return isRed;
	}

現在借助可空器Optional,支持一路過來的方法調用,先調用ofNullable方法設置對象實例,再調用map方法轉換數據類型,再調用orElse方法設置空指針之時的取值,最後調用equals方法進行顏色對比。采取Optional形式的方法代碼示例如下:

	// 判斷是否紅蘋果
	public boolean isRedApple() {
		// 利用Optional進行可空對象的處理,可空對象指的是該對象可能不存在(空指針)
		boolean isRed = Optional.ofNullable(this.color) // 構造一個可空對象
				.map(color -> color.toLowerCase()) // map指定了非空時候的取值
				.orElse("null") // orElse設置了空指針時候的取值
				.equals("red"); // 再判斷是否紅蘋果
		return isRed;
	}

然而上面Optional方式的代碼行數明顯超過了條件分支語句,它的先進性又何從體現呢?其實可選器並非要完全取代原先的空指針判斷,而是提供了另一種解決問題的新思路,通過合理搭配各項技術,方能取得最優的解決辦法。仍以挑選紅蘋果為例,原本判斷元素非空的分支語句“if (item != null)”,采用Optional改進之後的循環代碼如下所示:

	// 把for循環的內部代碼改寫為Optional校驗方式
	private static void getRedAppleWithOptionalOne(List<Apple> list) {
		List<Apple> redAppleList = new ArrayList<Apple>();
		if (list != null) { // 判斷清單非空
			for (Apple item : list) { // 遍歷現有的蘋果清單
				if (Optional.ofNullable(item) // 構造一個可空對象
						.map(apple -> apple.isRedApple()) // map指定了item非空時候的取值
						.orElse(false)) { // orElse指定了item為空時候的取值
					redAppleList.add(item);
				}
			}
		}
		System.out.println("Optional1判斷 紅蘋果清單=" + redAppleList.toString());
	}

註意到以上代碼仍然存在形如“if (list != null)”的清單非空判斷,而且該分支後面還有要命的for循環,這下既要利用Optional的ifPresent方法輸入消費行為,又要使用流式處理的forEach方法遍歷每個元素。於是進一步改寫後的Optional代碼變成了下面這般:

	// 把清單的非空判斷代碼改寫為Optional校驗方式
	private static void getRedAppleWithOptionalTwo(List<Apple> list) {
		List<Apple> redAppleList = new ArrayList<Apple>();
		Optional.ofNullable(list) // 構造一個可空對象
			.ifPresent( // ifPresent指定了list非空時候的處理
				apples -> {
					apples.stream().forEach( // 對蘋果清單進行流式處理
							item -> {
								if (Optional.ofNullable(item) // 構造一個可空對象
										.map(apple -> apple.isRedApple()) // map指定了item非空時候的取值
										.orElse(false)) { // orElse指定了item為空時候的取值
									redAppleList.add(item);
								}
							});
				});
		System.out.println("Optional2判斷 紅蘋果清單=" + redAppleList.toString());
	}

雖然二度改進後的代碼已經消除了空指針判斷分支,但是依然留下是否為紅蘋果的校驗分支,僅存的if語句著實礙眼,幹脆一不做二不休引入流式處理的filter方法替換if語句。幾經修改得到了以下的最終優化代碼:

	// 聯合運用Optional校驗和流式處理
	private static void getRedAppleWithOptionalThree(List<Apple> list) {
		List<Apple> redAppleList = new ArrayList<Apple>();
		Optional.ofNullable(list) // 構造一個可空對象
				.ifPresent(apples -> { // ifPresent指定了list非空時候的處理
					// 從原始清單中篩選出紅蘋果清單
					redAppleList.addAll(apples.stream()
								.filter(a -> a != null) // 只挑選非空元素
								.filter(Apple::isRedApple) // 只挑選紅蘋果
								.collect(Collectors.toList())); // 返回結果清單
					});
		System.out.println("Optional3判斷 紅蘋果清單=" + redAppleList.toString());
	}

好不容易去掉了所有if和for語句,盡管代碼的總行數未有明顯減少,不過邏輯結構顯然變得更加清晰了。



更多Java技術文章參見《Java開發筆記(序)章節目錄》

Java開發筆記(七十七)使用Optional規避空指針異常