Java函數式開發——優雅的Optional空指針處理

那些年困擾着咱們的null

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

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

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

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、EntityPOJA整合,而無需改動什麼,也能更輕鬆的整合到第三方接口中(例如springbean)。建議目前仍是以第一種Optional的使用方法爲主,畢竟不是團隊中每個人都能理解每一個get/set帶着一個Optional的用意。

    Optional還提供了一個filter方法用於過濾數據(實際上Java8stream風格的接口都提供了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除了上面提到的方法,還有orElseGetorElseThrow等根據更多須要提供的方法。orElseGet會由於出現null值拋出空指針異常,而orElseThrow會在出現null時,拋出一個使用者自定義的異常。能夠查看API文檔來了解全部方法的細節。

寫在最後的

    Optional只是Java函數式編程的冰山一角,須要結合lambdastreamFuncationinterface等特性才能真正的瞭解Java8函數式編程的效用。原本還想介紹一些Optional的源碼和運行原理的,可是Optional自己的代碼就不多、API接口也很少,仔細想一想也沒什麼好說的就省略了。

    Optional雖然優雅,可是我的感受有一些效率問題,不過還沒去驗證。若是有誰有確實的數據,請告訴我。

    本人也不是「函數式編程支持者」。從團隊管理者的角度來講,每提高一點學習難度,人員的使用成本和團隊交互成本就會更高一些。就像在傳說中Lisp能夠比C++的代碼量少三十倍、開發更高效,可是若一個國內的常規IT公司真用Lisp來作項目,請問去哪、得花多少錢弄到這些用Lisp的哥們啊?

    可是我很是鼓勵你們都學習和了解函數式編程的思路。尤爲是過去只侵淫在Java這一門語言、到如今還不清楚Java8會帶來什麼改變的開發人員,Java8是一個良好的契機。更鼓勵把新的Java8特性引入到目前的項目中,一個長期配合的團隊以及一門古老的編程語言都須要不斷的注入新活力,不然不進則退。

相關文章
相關標籤/搜索