天天看點

Java Optional空指針處理

那些年困擾着我們的null

在Java江湖流傳着這樣一個傳說:直到真正了解了空指針異常,才能算一名合格的Java開發人員。在我們逼格閃閃的java碼字元生涯中,每天都會遇到各種null的處理,像下面這樣的代碼可能我們每天都在反複編寫:

if(null != obj1){
  if(null != obje2){
     // do something
  }
}           

稍微有點眼界javaer就去幹一些稍有逼格的事,弄一個判斷null的方法:

boolean checkNotNull(Object obj){
  return null == obj ? false : true;
}

void do(){
  if(checkNotNull(obj1)){
     if(checkNotNull(obj2)){
        //do something
     }
  }
}           

然後,問題又來了:如果一個null表示一個空字元串,那""表示什麼?

然後慣性思維告訴我們,""和null不都是空字元串碼?索性就把判斷空值更新了一下:

boolean checkNotBlank(Object obj){
  return null != obj && !"".equals(obj) ? true : false;
}
void do(){
  if(checkNotBlank(obj1)){
     if(checkNotNull(obj2)){
        //do something
     }
  }
}           

有空的話各位可以看看目前項目中或者自己過往的代碼,到底寫了多少和上面類似的代碼。

不知道你是否認真思考過一個問題:一個null到底意味着什麼?

  1. 淺顯的認識——null當然表示“值不存在”。
  2. 對記憶體管理有點經驗的了解——null表示記憶體沒有被配置設定,指針指向了一個空位址。
  3. 稍微透徹點的認識——null可能表示某個地方處理有問題了,也可能表示某個值不存在。
  4. 被虐千萬次的認識——哎喲,又一個NullPointerException異常,看來我得加一個if(null != value)了。

回憶一下,在咱們前面碼字生涯中到底遇到過多少次java.lang.NullPointerException異常?NullPointerException作為一個RuntimeException級别的異常不用顯示捕獲,若不小心處理我們經常會在生産日志中看到各種由NullPointerException引起的異常堆棧輸出。而且根據這個異常堆棧資訊我們根本無法定位到導緻問題的原因,因為并不是抛出NullPointerException的地方引發了這個問題。我們得更深處去查詢什麼地方産生了這個null,而這個時候日志往往無法跟蹤。

有時更悲劇的是,産生null值的地方往往不在我們自己的項目代碼中。這就存在一個更尴尬的事實——在我們調用各種良莠不齊第三方接口時,說不清某個接口在某種機緣巧合的情況下就會傳回一個null……

回到前面對null的認知問題。很多javaer認為null就是表示“什麼都沒有”或者“值不存在”。按照這個慣性思維我們的代碼邏輯就是:你調用我的接口,按照你給我的參數傳回對應的“值”,如果這條件沒法找到對應的“值”,那我當然傳回一個null給你表示沒有“任何東西”了。我們看看下面這個代碼,用很傳統很标準的Java編碼風格編寫:

class MyEntity{
   int id;
   String name;
   String getName(){
      return name;
   }
}

// main
public class Test{
   public static void main(String[] args)
       final MyEntity myEntity = getMyEntity(false);
       System.out.println(myEntity.getName());
   }

   private getMyEntity(boolean isSuc){
       if(isSuc){
           return new MyEntity();
       }else{
           return null;
       }
   }
}           

這一段代碼很簡單,日常的業務代碼肯定比這個複雜的多,但是實際上我們大量的Java編碼都是按這種套路編寫的,懂貨的人一眼就可以看出最終肯定會抛出NullPointerException。但是在我們編寫業務代碼時,很少會想到要處理這個可能會出現的null(也許API文檔已經寫得很清楚在某些情況下會傳回null,但是你確定你會認真看完API文檔後才開始寫代碼麼?),直到我們到了某個測試階段,突然蹦出一個NullPointerException異常,我們才意識到原來我們得像下面這樣加一個判斷來搞定這個可能會傳回的null值。

// main
public class Test{
   public static void main(String[] args)
       final MyEntity myEntity = getMyEntity(false);
       if(null != myEntity){
           System.out.println(myEntity.getName());
       }else{
           System.out.println("ERROR");
       }
   }
}           

仔細想想過去這麼些年,咱們是不是都這樣幹過來的?如果直到測試階段才能發現某些null導緻的問題,那麼現在問題就來了——在那些雍容繁雜、層次分明的業務代碼中到底還有多少null沒有被正确處理呢?

對于null的處理态度,往往可以看出一個項目的成熟和嚴謹程度。比如Guava早在JDK1.6之前就給出了優雅的null處理方式,可見功底之深。

鬼魅一般的null阻礙我們進步

如果你是一位聚焦于傳統面向對象開發的Javaer,或許你已經習慣了null帶來的種種問題。但是早在許多年前,大神就說了null這玩意就是個坑。

托尼.霍爾(你不知道這貨是誰嗎?自己去查查吧)曾經說過:“I call it my billion-dollar mistake. It was the invention of the null reference in 1965. I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement.”(大意是:“哥将發明null這事稱為價值連城的錯誤。因為在1965那個計算機的蠻荒時代,空引用太容易實作,讓哥根本經不住誘惑發明了空指針這玩意。”)。

然後,我們再看看null還會引入什麼問題。

看看下面這個代碼:

String address = person.getCountry().getProvince().getCity();           

如果你玩過一些函數式語言(Haskell、Erlang、Clojure、Scala等等),上面這樣是一種很自然的寫法。用Java當然也可以實作上面這樣的編寫方式。

但是為了完滿的處理所有可能出現的null異常,我們不得不把這種優雅的函數程式設計範式改為這樣:

if (person != null) {
	Country country = person.getCountry();
	if (country != null) {
		Province province = country.getProvince();
		if (province != null) {
			address = province.getCity();
		}
	}
}           

瞬間,高逼格的函數式程式設計Java8又回到了10年前。這樣一層一層的嵌套判斷,增加代碼量和不優雅還是小事。更可能出現的情況是:在大部分時間裡,人們會忘記去判斷這可能會出現的null,即使是寫了多年代碼的老人家也不例外。

上面這一段層層嵌套的 null 處理,也是傳統Java長期被诟病的地方。如果以Java早期版本作為你的啟蒙語言,這種get->if null->return 的臭毛病會影響你很長的時間(記得在某國外社群,這被稱為:面向entity開發)。

利用Optional實作Java函數式程式設計

好了,說了各種各樣的毛病,然後我們可以進入新時代了。

早在推出Java SE 8版本之前,其他類似的函數式開發語言早就有自己的各種解決方案。下面是Groovy的代碼:

String version = computer?.getSoundcard()?.getUSB()?.getVersion():"unkonwn";           

Haskell用一個 Maybe 類型類辨別處理null值。而号稱多範式開發語言的Scala則提供了一個和Maybe差不多意思的Option[T],用來包裹處理null。

Java8引入了 java.util.Optional<T>來處理函數式程式設計的null問題,Optional<T>的處理思路和Haskell、Scala類似,但又有些許差別。先看看下面這個Java代碼的例子:

public class Test {
	public static void main(String[] args) {
		final String text = "Hallo world!";
		Optional.ofNullable(text)//顯示建立一個Optional殼
		    .map(Test::print)
			.map(Test::print)
			.ifPresent(System.out::println);

		Optional.ofNullable(text)
			.map(s ->{ 
				System.out.println(s);
				return s.substring(6);
			})
			.map(s -> null)//傳回 null
			.ifPresent(System.out::println);
	}
	// 列印并截取str[5]之後的字元串
	private static String print(String str) {
		System.out.println(str);
		return str.substring(6);
	}
}
//Consol 輸出
//num1:Hallo world!
//num2:world!
//num3:
//num4:Hallo world!           

(可以把上面的代碼copy到你的IDE中運作,前提是必須安裝了JDK8。)

上面的代碼中建立了2個Optional,實作的功能基本相同,都是使用Optional作為String的外殼對String進行截斷處理。當在處理過程中遇到null值時,就不再繼續處理。我們可以發現第二個Optional中出現s->null之後,後續的ifPresent不再執行。

注意觀察輸出的 //num3:,這表示輸出了一個""字元,而不是一個null。

Optional提供了豐富的接口來處理各種情況,比如可以将代碼修改為:

public class Test {
	public static void main(String[] args) {
		final String text = "Hallo World!";
		System.out.println(lowerCase(text));//方法一
		lowerCase(null, System.out::println);//方法二
	}

	private static String lowerCase(String str) {
		return Optional.ofNullable(str).map(s -> s.toLowerCase()).map(s->s.replace("world", "java")).orElse("NaN");
	}

	private static void lowerCase(String str, Consumer<String> consumer) {
		consumer.accept(lowerCase(str));
	}
}
//輸出
//hallo java!
//NaN           

這樣,我們可以動态的處理一個字元串,如果在任何時候發現值為null,則使用orElse傳回預設預設的"NaN"。

總的來說,我們可以将任何資料結構用Optional包裹起來,然後使用函數式的方式對他進行處理,而不必關心随時可能會出現的null。

我們看看前面提到的Person.getCountry().getProvince().getCity()怎麼不用一堆if來處理。

第一種方法是不改變以前的entity:

import java.util.Optional;
public class Test {
	public static void main(String[] args) {
		System.out.println(Optional.ofNullable(new Person())
			.map(x->x.country)
			.map(x->x.provinec)
			.map(x->x.city)
			.map(x->x.name)
			.orElse("unkonwn"));
	}
}
class Person {
	Country country;
}
class Country {
	Province provinec;
}
class Province {
	City city;
}
class City {
	String name;
}           

這裡用Optional作為每一次傳回的外殼,如果有某個位置傳回了null,則會直接得到"unkonwn"。

第二種辦法是将所有的值都用Optional來定義:

import java.util.Optional;
public class Test {
	public static void main(String[] args) {
		System.out.println(new Person()
				.country.flatMap(x -> x.provinec)
				.flatMap(Province::getCity)
				.flatMap(x -> x.name)
				.orElse("unkonwn"));
	}
}
class Person {
	Optional<Country> country = Optional.empty();
}
class Country {
	Optional<Province> provinec;
}
class Province {
	Optional<City> city;
	Optional<City> getCity(){//用于::
		return city;
	}
}
class City {
	Optional<String> name;
}           

第一種方法可以平滑的和已有的JavaBean、Entity或POJA整合,而無需改動什麼,也能更輕松的整合到第三方接口中(例如spring的bean)。建議目前還是以第一種Optional的使用方法為主,畢竟不是團隊中每一個人都能了解每個get/set帶着一個Optional的用意。

Optional還提供了一個filter方法用于過濾資料(實際上Java8裡stream風格的接口都提供了filter方法)。例如過去我們判斷值存在并作出相應的處理:

if(Province!= null){
  City city = Province.getCity();
  if(null != city && "guangzhou".equals(city.getName()){
    System.out.println(city.getName());
  }else{
    System.out.println("unkonwn");
  }
}           

    現在我們可以修改為

Optional.ofNullable(province)
   .map(x->x.city)
   .filter(x->"guangzhou".equals(x.getName()))
   .map(x->x.name)
   .orElse("unkonw");           

到此,利用Optional來進行函數式程式設計介紹完畢。Optional除了上面提到的方法,還有orElseGet、orElseThrow等根據更多需要提供的方法。orElseGet會因為出現null值抛出空指針異常,而orElseThrow會在出現null時,抛出一個使用者自定義的異常。可以檢視API文檔來了解所有方法的細節。

寫在最後的

Optional隻是Java函數式程式設計的冰山一角,需要結合lambda、stream、Funcationinterface等特性才能真正的了解Java8函數式程式設計的效用。本來還想介紹一些Optional的源碼和運作原理的,但是Optional本身的代碼就很少、API接口也不多,仔細想想也沒什麼好說的就省略了。

Optional雖然優雅,但是個人感覺有一些效率問題,不過還沒去驗證。如果有誰有确實的資料,請告訴我。

本人也不是“函數式程式設計支援者”。從團隊管理者的角度來說,每提升一點學習難度,人員的使用成本和團隊互動成本就會更高一些。就像在傳說中Lisp可以比C++的代碼量少三十倍、開發更高效,但是若一個國内的正常IT公司真用Lisp來做項目,請問去哪、得花多少錢弄到這些用Lisp的哥們啊?

但是我非常鼓勵大家都學習和了解函數式程式設計的思路。尤其是過去隻侵淫在Java這一門語言、到現在還不清楚Java8會帶來什麼改變的開發人員,Java8是一個良好的契機。更鼓勵把新的Java8特性引入到目前的項目中,一個長期配合的團隊以及一門古老的程式設計語言都需要不斷的注入新活力,否則不進則退。