一、函數式編程
函數式編程,同面向對象編程、指令式編程一樣,是一種軟件編程范式,在多種編程語言中都有應用。百科詞條中有很學術化的解釋,但理解起來並不容易。不過,我們可以借助於數學中函數的概念,來理解函數式編程的要義所在。在數學中,我們常見的函數表達式形如 y=f(x),表示的是一種輸入輸出的映射關系:x表示輸入,y表示輸出,f 是表示兩者之間的映射運算邏輯。在求值的時候,你完全不用考慮映射運算 f,只要給定輸入 x,得到相應的輸出 y;輸入不變,輸出也不會改變,就這么簡單。類比到程序語言中來,所謂函數式編程,就是讓我們以數學中函數映射的思想來編寫出函數式的程序代碼,讓代碼着重於輸入和輸出,而底層的映射處理邏輯,你完全可以當黑盒看待,這樣,我們的業務關注點會更加清晰;而且,同數學函數一樣,函數式編程的代碼具有狀態無關性——即相同的輸入永遠產生相同的輸出,這在解決並發編程中共享變量狀態一致性問題中有很大的應用場景。
在Java中,提到函數式編程,最先想到的肯定是Lambda表達式了(PS:切忌把Lambda表達式和函數式編程划等號,Lambda表達式只是符合這種函數式編程風格的匿名函數而已)。Lambda表達式在Java8中終於被重磅引入了(隔壁Python,C#,C++早就引入了喲喂),這讓很多以前代碼中的匿名寫法得以通過函數式的代碼進行極致的簡化,有多簡化呢?比如使用IDEA開發的時候,如果你選擇的Java編譯版本達到Java8的話,在編寫匿名內部類的時候,編譯器會不厭其煩的提示你將匿名寫法替換成Lambda表達式——
你替換以后,原來幾行的代碼就簡化成了下面這個樣子——
這就是讓你初見懵逼,再見着迷的函數式編程范例了。常有人說,相較於匿名內部類的寫法,Lambda 表達式使代碼更加簡潔、易讀。簡潔確實簡潔,畢竟減少了很多樣板代碼,非要說易讀,博主是有些遲疑的。從語法上來說,除非你對Java8的這種新特性有過相當的學習,否則剛開始接觸這種寫法,你反而會有些凌亂,不解其意。喜感的是,這種只強調輸入和輸出的函數式編程風格其顯著優點恰是讓你一見代碼就知其業務內容;你之所以會凌亂,在於其語法相較於我們面向對象編程中熟悉的類、接口、字段等概念太過新穎了,需要一定的學習成本。
在上面的代碼示例中,我們之所以寫匿名內部類,是因為在單一業務場景中,我們不想額外的編寫接口的實現再去構造對象執行方法,而是直接創建匿名對象,執行完接口中的方法,對象的使命也就結束了。相較於先實現再建對象的方式,匿名內部類的寫法算是一種代碼的減省,雖然可讀性差了些,但咱不怕!難受的一點在於,即使匿名寫法,依然要遵循接口實現的規范,會多出很多樣板式的代碼;而實際上,我不過是想基於接口的方法定義去實現某種行為而已。也就是說,我的關注點在於接口的行為實現,而不是樣板的語法層面。這個時候,Lambda表達式就得以大顯身手了,它如你所願,讓你以函數式的編碼風格,只關注行為本身的邏輯實現,其它的無關代碼通通舍棄。就像上面的示例中,將傳統的匿名寫法改成 Lambda 表達式寫法后,樣板代碼沒了,簡潔的代碼讓你一眼就能看出,你的代碼要干什么。——這,就是Lambda!雖然初見時確實有些語法障礙,但在突破障礙之后,你會從心的喜歡這種編程方式——至少,在你的代碼走位中應該適時的加入些 Lambda 這種風騷的‘姿勢’了。
有人說,不就是代碼簡化嘛,語法糖而已啦。關於 Lambda 表達式是不是語法糖的說法,又可以開篇講義了。只能簡單的說,Lambda 確實也是語法糖,但絕不是簡單只為簡化匿名內部類的寫法的語法糖。當然,除了國內大神們的探究,你也可以去 Stack Overflow上提問或搜尋答案,看看老外們都怎么解釋的。
二、Lambda
Lambda 表達式的個人理解,其實上文中已經給出了。現在,我們從語法層面,來說說實際項目中該如何編寫基於 Lambda 的函數式風格代碼。博主以上面的代碼為例,整了一副草圖,幫助你快速讀懂 Lambda 語法:
這只是最簡單的形式。空括號 () 表示沒有輸入參數,如果匿名接口有參數,你按照正常方法的參數定義編寫即可,如 (Object o),(Object o1,Object o2)等。但由於Java7 開始就有了類型推斷,通常我們是可以省略參數類型的,所以參數可以簡化成 (o) ,(o1,o2)的形式,甚至在只有一個參數的時候,括號也能省略而只保留參數 o 。至於表達式的主體部分,也就是我們的業務代碼,既可以是是一個表達式,如上面的一條打印,也可以是用花括號 { } 包圍的一段代碼塊,具體以實際業務為准,當然,也要考慮代碼的可讀性。最后列舉一些常見 Lambda 代碼示例:
// 開啟普通的線程任務 new Thread(() -> System.out.println("函數式編程——陳本布衣")).start(); // 可以是代碼塊的形式 new Thread(() ->{ System.out.println("函數式編程中的代碼塊"); System.out.println("函數式編程中的代碼塊"); System.out.println("函數式編程中的代碼塊"); System.out.println("函數式編程中的代碼塊"); }).start(); // 開啟異步單線程,可獲取線程任務返回值 Future<Integer> future = Executors.newSingleThreadExecutor().submit(() -> 10); // GUI圖形界面編程中的事件處理 new JButton().addActionListener((ActionEvent event) -> System.out.println("按鈕點擊事件")); // 參數根據上下文推斷,單參數可省略括號 () new JButton().addActionListener(event -> System.out.println("按鈕點擊事件"));
友情提示:由於 Lambda對代碼的極致簡化和新語法,初學者很難一步到位的寫出正確的 Lambda 表達式代碼,對初學者,比較好的實踐建議先用匿名內部類的形式先實現,最后借助於IDE的快捷功能自動生成,待熟練之后,再裝逼不遲!
三、函數接口
只學會了 Lambda 表達式的語法還遠遠不夠,因為你不光要能手擼 Lambda 表達式代碼,更重要的是你要搞清楚,在哪種場景下可以擼,哪種場景下無法擼,這是有講究的。雖然上文中舉了幾個示例,但在實際應用中是遠遠不夠的。博主說過,Lambda 表達式本質上是一個匿名函數,這么說,難道只要接口采用匿名類實現的地方,都可以使用Lambda 嗎?答案當然是否定的!你可以親自試一下,自己編寫一個多方法的接口,也采用匿名實現,你看IDEA會不會那么熱情的提醒你。
其實,在Java8 中伴隨 Lambda 一起引入的,還有函數式接口這一概念。所謂函數式接口,是只有一個抽象方法的接口,只有這種接口才能被用來作為 Lambda 表達式的類型——也就是說,只有函數式接口的匿名實現,你才可以用 Lambda 表達式去改寫代碼。感覺這種限定縮窄了Lambda的應用范圍,我上哪兒給你找那么多只有一個抽象函數的接口啊?有的,有的,而且還不少。Java8 為了支持函數式編程的應用場景,特意新增加了一個全是函數式接口的包 java.util.function,里面包含了四十多個函數式接口,足夠你玩一陣子的了,而且這還不包括舊有的接口中符合函數接口定義的眾多接口,如 Runnable,Comparator<T>,以及Java8中為了更便利的操作集合而新增的特性類庫等。從 Java8 開始,你在源碼中可以發現,無論舊有的和新引入的函數式接口,其接口聲明上都會有 @FunctionalInterface 注解,該注解其實就是專門用來標注函數式接口的,算是一個標識注解。當然,也不是說函數接口就一定要用標示 @FunctionalInterface 注解來標注,只要符合只有一個抽象方法的接口定義,沒有 @FunctionalInterface 標注也能成為 Lambda表達式的類型,只不過在接口上加上注解(尤其自己在定義函數式接口的時候),可以讓編譯器幫你檢查錯誤。
函數接口,說這么多其實差不多就算完整了,但是且慢,博主還是要糾結一下:只有一個抽象方法的接口,是為函數式接口,那么,是不是不止一個抽象方法的接口,就一定不是抽象接口呢?上一段的闡述中,布衣博主故意列了一個 Comparator<T>接口,其在Java8 中的源碼如下:
@FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); // 省略非抽象的 靜態 和 默認方法 。。。 }
咦,這廝不對啊,有兩個抽象接口,怎么也成了函數式接口?遇到這種情況,切莫慌張,找尋答案最好的方式,還是要從該接口聲明的注釋中去找,萬一寫源碼的人搞錯了呢?當然,錯肯定是錯不了,不過 Comparator 接口聲明的注釋中也沒有給出合理的解釋,還是只能從源頭 @FunctionalInterface 注解的注釋中去看看有沒有答案。在 @FunctionalInterface 注解的注釋文檔中你會找到這樣的描述:
If an interface declares an abstract method overriding one of the public methods of {@code java.lang.Object}, that also does not count toward the interface's abstract method count since any implementation of the interface will have an implementation from {@code java.lang.Object} or elsewhere。
這算是很白話的英語了,簡單翻譯一下就是,因為所有接口都是默認繼承了Object類的,所以,接口中如果有自 Object 類中繼承而來的public方法,就不能算成抽象方法了。
好啦,對於函數式編程講解的的開篇,算是講完了。但這僅僅是開始,對於函數式編程這樣一種新的編程嘗試,還有很多值得學習和討論的地方。后續博主會繼續深入探究 Java8 中針對函數式編程引入的一些方法類庫,以及這些新特性能給我們的編碼帶來哪些便利。
限於法力有限,只能粗淺講解;歡迎挑刺,不勝感激。