Java如何支持函數(shù)式編程?
Java是面向對象的語言,無法直接調用一個函數(shù)。Java 8開始,引入了函數(shù)式編程接口與Lambda表達式,便于開發(fā)者寫出更少更優(yōu)雅的代碼。什么是函數(shù)式編程?函數(shù)式編程的特點是什么?本文通過代碼實例,從Stream類、Lambda表達式和函數(shù)接口這三個語法概念來分享Java對函數(shù)式編程的支持。
背景
在很長的一段時間里,Java一直是面向對象的語言,一切皆對象,如果想要調用一個函數(shù),函數(shù)必須屬于一個類或對象,然后在使用類或對象進行調用。但是在其它的編程語言中,如JS、C++,我們可以直接寫一個函數(shù),然后在需要的時候進行調用,既可以說是面向對象編程,也可以說是函數(shù)式編程。從功能上來看,面向對象編程沒什么不好的地方,但是從開發(fā)的角度來看,面向對象編程會多寫很多可能是重復的代碼行。比如創(chuàng)建一個Runnable的匿名類的時候:
這一段代碼中真正有用的只有run方法中的內容,剩余的部分都是屬于Java編程語言的結構部分,沒什么用,但是要寫。幸運的是Java 8開始,引入了函數(shù)式編程接口與Lambda表達式,幫助我們寫更少更優(yōu)雅的代碼:
現(xiàn)在主流的編程范式主要有三種,面向過程、面向對象和函數(shù)式編程。
函數(shù)式編程并非一個很新的東西,早在50多年前就已經(jīng)出現(xiàn)了。近幾年,函數(shù)式編程越來越被人關注,出現(xiàn)了很多新的函數(shù)式編程語言,比如Clojure、Scala、Erlang等。一些非函數(shù)式編程語言也加入了很多特性、語法、類庫來支持函數(shù)式編程,比如Java、Python、Ruby、JavaScript等。除此之外,Google Guava也有對函數(shù)式編程的增強功能。
函數(shù)式編程因其編程的特殊性,僅在科學計算、數(shù)據(jù)處理、統(tǒng)計分析等領域,才能更好地發(fā)揮它的優(yōu)勢,所以它并不能完全替代更加通用的面向對象編程范式。但是作為一種補充,它也有很大存在、發(fā)展和學習的意義。
什么是函數(shù)式編程
函數(shù)式編程的英文翻譯是Functional Programming。
那到底什么是函數(shù)式編程呢?實際上,函數(shù)式編程沒有一個嚴格的官方定義。嚴格上來講,函數(shù)式編程中的“函數(shù)”,并不是指我們編程語言中的“函數(shù)”概念,而是指數(shù)學“函數(shù)”或者“表達式”(例如:y=f(x))。不過,在編程實現(xiàn)的時候,對于數(shù)學“函數(shù)”或“表達式”,我們一般習慣性地將它們設計成函數(shù)。所以,如果不深究的話,函數(shù)式編程中的“函數(shù)”也可以理解為編程語言中的“函數(shù)”。
每個編程范式都有自己獨特的地方,這就是它們會被抽象出來作為一種范式的原因。面向對象編程最大的特點是:以類、對象作為組織代碼的單元以及它的四大特性。面向過程編程最大的特點是:以函數(shù)作為組織代碼的單元,數(shù)據(jù)與方法相分離。那函數(shù)式編程最獨特的地方又在哪里呢?實際上,函數(shù)式編程最獨特的地方在于它的編程思想。函數(shù)式編程認為程序可以用一系列數(shù)學函數(shù)或表達式的組合來表示。函數(shù)式編程是程序面向數(shù)學的更底層的抽象,將計算過程描述為表達式。不過,這樣說你肯定會有疑問,真的可以把任何程序都表示成一組數(shù)學表達式嗎?
理論上講是可以的。但是,并不是所有的程序都適合這么做。函數(shù)式編程有它自己適合的應用場景,比如科學計算、數(shù)據(jù)處理、統(tǒng)計分析等。在這些領域,程序往往比較容易用數(shù)學表達式來表示,比起非函數(shù)式編程,實現(xiàn)同樣的功能,函數(shù)式編程可以用很少的代碼就能搞定。但是,對于強業(yè)務相關的大型業(yè)務系統(tǒng)開發(fā)來說,費勁吧啦地將它抽象成數(shù)學表達式,硬要用函數(shù)式編程來實現(xiàn),顯然是自討苦吃。相反,在這種應用場景下,面向對象編程更加合適,寫出來的代碼更加可讀、可維護。
再具體到編程實現(xiàn),函數(shù)式編程跟面向過程編程一樣,也是以函數(shù)作為組織代碼的單元。不過,它跟面向過程編程的區(qū)別在于,它的函數(shù)是無狀態(tài)的。何為無狀態(tài)?簡單點講就是,函數(shù)內部涉及的變量都是局部變量,不會像面向對象編程那樣,共享類成員變量,也不會像面向過程編程那樣,共享全局變量。函數(shù)的執(zhí)行結果只與入?yún)⒂嘘P,跟其他任何外部變量無關。同樣的入?yún)ⅲ还茉趺磮?zhí)行,得到的結果都是一樣的。這實際上就是數(shù)學函數(shù)或數(shù)學表達式的基本要求。舉個例子:
不同的編程范式之間并不是截然不同的,總是有一些相同的編程規(guī)則。比如不管是面向過程、面向對象還是函數(shù)式編程,它們都有變量、函數(shù)的概念,最頂層都要有main函數(shù)執(zhí)行入口,來組裝編程單元(類、函數(shù)等)。只不過,面向對象的編程單元是類或對象,面向過程的編程單元是函數(shù),函數(shù)式編程的編程單元是無狀態(tài)函數(shù)。
Java對函數(shù)式編程的支持
實現(xiàn)面向對象編程不一定非得使用面向對象編程語言,同理,實現(xiàn)函數(shù)式編程也不一定非得使用函數(shù)式編程語言。現(xiàn)在,很多面向對象編程語言,也提供了相應的語法、類庫來支持函數(shù)式編程。
Java這種面向對象編程語言,對函數(shù)式編程的支持可以通過一個例子來描述:
這段代碼的作用是從一組字符串數(shù)組中,過濾出長度小于等于3的字符串,并且求得這其中的最大長度。
Java為函數(shù)式編程引入了三個新的語法概念:Stream類、Lambda表達式和函數(shù)接口(Functional Inteface)。Stream類用來支持通過“.”級聯(lián)多個函數(shù)操作的代碼編寫方式;引入Lambda表達式的作用是簡化代碼編寫;函數(shù)接口的作用是讓我們可以把函數(shù)包裹成函數(shù)接口,來實現(xiàn)把函數(shù)當做參數(shù)一樣來使用(Java 不像C那樣支持函數(shù)指針,可以把函數(shù)直接當參數(shù)來使用)。
Stream類
假設我們要計算這樣一個表達式:(3-1)*2+5。如果按照普通的函數(shù)調用的方式寫出來,就是下面這個樣子:
不過,這樣編寫代碼看起來會比較難理解,我們換個更易讀的寫法,如下所示:
在Java中,“.”表示調用某個對象的方法。為了支持上面這種級聯(lián)調用方式,我們讓每個函數(shù)都返回一個通用的Stream類對象。在Stream類上的操作有兩種:中間操作和終止操作。中間操作返回的仍然是Stream類對象,而終止操作返回的是確定的值結果。
再來看之前的例子,對代碼做了注釋解釋。其中map、filter是中間操作,返回Stream類對象,可以繼續(xù)級聯(lián)其他操作;max是終止操作,返回的不是Stream類對象,無法再繼續(xù)往下級聯(lián)處理了。
Lambda表達式
前面提到Java引入Lambda表達式的主要作用是簡化代碼編寫。實際上,我們也可以不用Lambda表達式來書寫例子中的代碼。我們拿其中的map函數(shù)來舉例說明。
下面三段代碼,第一段代碼展示了map函數(shù)的定義,實際上,map函數(shù)接收的參數(shù)是一個Function接口,也就是函數(shù)接口。第二段代碼展示了map函數(shù)的使用方式。第三段代碼是針對第二段代碼用Lambda表達式簡化之后的寫法。實際上,Lambda表達式在Java中只是一個語法糖而已,底層是基于函數(shù)接口來實現(xiàn)的,也就是第二段代碼展示的寫法。
Lambda表達式包括三部分:輸入、函數(shù)體、輸出。表示出來的話就是下面這個樣子:
實際上,Lambda表達式的寫法非常靈活。上面給出的是標準寫法,還有很多簡化寫法。比如,如果輸入?yún)?shù)只有一個,可以省略 (),直接寫成 a->{…};如果沒有入?yún)ⅲ梢灾苯訉⑤斎牒图^都省略掉,只保留函數(shù)體;如果函數(shù)體只有一個語句,那可以將{}省略掉;如果函數(shù)沒有返回值,return語句就可以不用寫了。
Lambda表達式與匿名類的異同集中體現(xiàn)在以下三點上:
- Lambda就是為了優(yōu)化匿名內部類而生,Lambda要比匿名類簡潔的多得多。
- Lambda僅適用于函數(shù)式接口,匿名類不受限。
- 即匿名類中的this是“匿名類對象”本身;Lambda表達式中的this是指“調用Lambda表達式的對象”。
函數(shù)接口
實際上,上面一段代碼中的Function、Predicate、Comparator都是函數(shù)接口。我們知道,C語言支持函數(shù)指針,它可以把函數(shù)直接當變量來使用。
但是,Java沒有函數(shù)指針這樣的語法。所以它通過函數(shù)接口,將函數(shù)包裹在接口中,當作變量來使用。實際上,函數(shù)接口就是接口。不過,它也有自己特別的地方,那就是要求只包含一個未實現(xiàn)的方法。因為只有這樣,Lambda表達式才能明確知道匹配的是哪個方法。如果有兩個未實現(xiàn)的方法,并且接口入?yún)ⅰ⒎祷刂刀家粯樱荍ava在翻譯Lambda表達式的時候,就不知道表達式對應哪個方法了。
函數(shù)式接口也是Java interface的一種,但還需要滿足:
- 一個函數(shù)式接口只有一個抽象方法(single abstract method);
- Object類中的public abstract method不會被視為單一的抽象方法;
- 函數(shù)式接口可以有默認方法和靜態(tài)方法;
- 函數(shù)式接口可以用@FunctionalInterface注解進行修飾。
滿足這些條件的interface,就可以被視為函數(shù)式接口。例如Java 8中的Comparator接口:
函數(shù)式接口有什么用呢?一句話,函數(shù)式接口帶給我們最大的好處就是:可以使用極簡的lambda表達式實例化接口。為什么這么說呢?我們或多或少使用過一些只有一個抽象方法的接口,比如Runnable、ActionListener、Comparator等等,比如我們要用Comparator實現(xiàn)排序算法,我們的處理方式通常無外乎兩種:
- 規(guī)規(guī)矩矩的寫一個實現(xiàn)了Comparator接口的Java類去封裝排序邏輯。若業(yè)務需要多種排序方式,那就得寫多個類提供多種實現(xiàn),而這些實現(xiàn)往往只需使用一次。
- 另外一種聰明一些的做法無外乎就是在需要的地方搞個匿名內部類,比如:
匿名內部類實現(xiàn)的代碼量沒有多到哪里去,結構也還算清晰。Comparator接口在Jdk 1.8的實現(xiàn)增加了FunctionalInterface注解,代表Comparator是一個函數(shù)式接口,使用者可放心的通過lambda表達式來實例化。那我們來看看使用lambda表達式來快速new一個自定義比較器所需要編寫的代碼:
-> 前面的 () 是Comparator接口中compare方法的參數(shù)列表,-> 后面則是compare方法的方法體。
下面將Java提供的Function、Predicate這兩個函數(shù)接口的源碼,摘抄如下:
@FunctionalInterface注解使用場景
我們知道,一個接口只要滿足只有一個抽象方法的條件,即可以當成函數(shù)式接口使用,有沒有 @FunctionalInterface 都無所謂。但是jdk定義了這個注解肯定是有原因的,對于開發(fā)者,該注解的使用一定要三思而后續(xù)行。
如果使用了此注解,再往接口中新增抽象方法,編譯器就會報錯,編譯不通過。換句話說,@FunctionalInterface 就是一個承諾,承諾該接口世世代代都只會存在這一個抽象方法。因此,凡是使用了這個注解的接口,開發(fā)者可放心大膽的使用Lambda來實例化。當然誤用 @FunctionalInterface 帶來的后果也是極其慘重的:如果哪天你把這個注解去掉,再加一個抽象方法,則所有使用Lambda實例化該接口的客戶端代碼將全部編譯錯誤。
特別地,當某接口只有一個抽象方法,但沒有用 @FunctionalInterface 注解修飾時,則代表別人沒有承諾該接口未來不增加抽象方法,所以建議不要用Lambda來實例化,還是老老實實的用以前的方式比較穩(wěn)妥。
小結
函數(shù)式編程更符合數(shù)學上函數(shù)映射的思想。具體到編程語言層面,我們可以使用Lambda表達式來快速編寫函數(shù)映射,函數(shù)之間通過鏈式調用連接到一起,完成所需業(yè)務邏輯。Java的Lambda表達式是后來才引入的,由于函數(shù)式編程在并行處理方面的優(yōu)勢,正在被大量應用在大數(shù)據(jù)計算領域。
【本文為51CTO專欄作者“阿里巴巴官方技術”原創(chuàng)稿件,轉載請聯(lián)系原作者】