程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 淺談Java 8的函數式編程

淺談Java 8的函數式編程

編輯:關於JAVA

關於“Java 8為Java帶來了函數式編程”已經有了很多討論,但這句話的真正意義是什麼?

本文將討論函數式,它對一種語言或編程方式意味著什麼。在回答“Java 8的函數式編程怎麼樣”之前,我們先看看Java的演變,特別是它的類型系統,我們將看到Java 8的新特性,特別是Lambda表達式如何改變Java的風景,並提供函數式編程風格的主要優勢。

函數式編程語言是什麼?

函數式編程語言的核心是它以處理數據的方式處理代碼。這意味著函數應該是第一等級(First-class)的值,並且能夠被賦值給變量,傳遞給函數等等。

事實上,很多函數式語言比這走得更遠,將計算和算法看得比它們操作的數據更重要。其中有些語言想分離程序狀態和函數(以一種看起來有點對立的方式,使用面向對象的語言,這通常會將它們聯系得更緊密)。

Clojure編程語言就是一個這樣的例子,盡管它運行於基於類的Java虛擬機,Clojure的本質是函數式語言,並且在高級語言源程序中不直接公布類和對象(盡管提供了與Java良好的互操作性)。

下面顯示的是一個Clojure函數,用於處理日志,是一等公民(First-class citizen),並且不需要綁定一個類而存在。

(defn build-map-http-entries [log-file]
 (group-by :uri (scan-log-for-http-entries log-file)))

當寫在函數中的程序,對給定的輸入(不論程序中的其它狀態如何)總是返回相同的輸出,並且不會產生其它影響,或者改變任何程序狀態,這時候函數式編程是最有用的。它們的行為與數學函數相同,有時候把遵循這個標准的函數稱為“純”函數。

純函數的巨大好處是它們更容易推論,因為它們的操作不依賴於外部狀態。函數能夠很容易地結合在一起,這在開發者工作流風格中很常見,例如Lisp方言和其它具有強函數傳統的語言中很普遍的REPL(Read, Execute, Print, Loop)風格。

非函數式編程語言中的函數式編程

一種語言是不是函數式並不是非此即彼的狀態,實際上,語言存在於圖譜上。在最末端,基本上是強制函數式編程,通常禁止可變的數據結構。Clojure就是一種不接受可變數據的語言。

不過,也有一些其它語言,通常以函數方式編程,但語言並不強制這一點。Scala就是一個例子,它混和了面向對象和函數式語言。允許函數作為值,例如:

val sqFn = (x: Int) => x * x

同時保留與Java非常接近的類和對象語法。

另一個極端,當然,使用完全非函數式語言進行函數式編程是可能的,例如C語言,只要維持好合適的程序員准則和慣例。

考慮到這一點,函數式編程應該被看作是有兩個因素的函數,其中一個與編程語言相關,另一個是用該語言編寫的程序:

1)底層編程語言在多大程度上支持,或者強制函數式編程?

2)這個特定的程序如何使用語言提供的函數式特性?它是否避免了非函數式特性,例如可變狀態?

Java的一些歷史

Java是一種固執己見的語言,它具有很好的可讀性,初級程序員很容易上手,具有長期穩定性和可支持性。但這些設計決定也付出了一定的代價:冗長的代碼,類型系統與其它語言相比顯得缺乏彈性。

然而,Java的類型系統已經在演化,雖然在語言的歷史當中相對比較慢。我們來看看這些年來它的一些形式。

Java最初的類型系統

Java最初的類型系統至今已經超過15年了。它簡單而清晰,類型包括引用類型和基本類型。類、接口或者數組屬於引用類型。

類是Java平台的核心,類是Java平台將會加載、或鏈接的功能的基本單位,所有要執行的代碼都必須駐留於一個類中。

接口不能直接實例化,而是要通過一個實現了接口API的類。

數組可以包含基本類型、類的實例或者其它數組。

基本類型全部由平台定義,程序員不能定義新的基本類型。

從最早開始,Java的類型系統一直堅持很重要的一點,每一種類型都必須有一個可以被引用的名字。這被稱為“標明類型(Nominative typing)”,Java是一種強標明類型語言。

即使是所謂的“匿名內部類”也仍然有類型,程序員必須能引用它們,才能實現那些接口類型:

Runnable r = new Runnable() { public void run() { System.out.println("Hello World!"); } };

換種說法,Java中的每個值要麼是基本類型,要麼是某個類的實例。

命名類型(Named Type)的其它選擇

其它語言沒有這麼迷戀命名類型。例如,Java沒有這樣的Scala概念,一個實現(特定簽名的)特定方法的類型。在Scala中,可以這樣寫:

x : {def bar : String}

記住,Scala在右側標示變量類型(冒號後面),所以這讀起來像是“x是一種類型,它有一個方法bar返回String”。我們能用它來定義類似這樣的Scala方法:

def showRefine(x : {def bar : String}) = { print(x.bar) }

然後,如果我們定義一個合適的Scala對象:

object barBell { def bar = "Bell" }

然後調用showRefine(barBell),這就是我們期待的事:

showRefine(barBell) Bell

這是一個精化類型(Refinement typing)的例子。從動態語言轉過來的程序員可能熟悉“鴨子類型(Duck typing)”。結構精化類型(Structural refinement typing)是類似的,除了鴨子類型(如果它走起來像鴨子,叫起來像鴨子,就可以把它當作鴨子)是運行時類型,而這些結構精化類型作用於編譯時。

在完全支持結構精化類型的語言中,這些精化類型可以用在程序員可能期望的任何地方,例如方法參數的類型。而Java,相反地,不支持這樣的類型(除了幾個稍微怪異的邊緣例子)。

Java 5類型系統

Java 5的發布為類型系統帶來了三個主要新特性,枚舉、注解和泛型。

枚舉類型(Enum)在某些方面與類相似,但是它的屬性只能是指定數量的實例,每個實例都不同並且在類描述中指定。主要用於“類型安全的常量”,而不是當時普遍使用的小整數常量,枚舉構造同時還允許附加的模式,有時候這非常有用。

注解(Annotation)與接口相關,聲明注解的關鍵字是@interface,以@開始表示這是個注解類型。正如名字所建議的,它們用於給Java代碼元素做注釋,提供附加信息,但不影響其行為。此前,Java曾使用“標記接口(Marker interface)”來提供這種元數據的有限形式,但注解被認為更有靈活性。

Java泛型提供了參數化類型,其想法是一種類型能扮演其它類型對象的“容器”,無需關心被包含類型的具體細節。裝配到容器中的類型通常稱為類型參數。

Java 5引入的特性中,枚舉和注解為引用類型提供了新的形式,這需要編譯器特殊處理,並且有效地從現有類型層級結構分離。

泛型為Java的類型系統增加了顯著額外的復雜性,不僅僅因為它們是純粹的編譯時特性,還要求Java開發人員應注意,編譯時和運行時的類型系統彼此略有不同。

盡管有這些變化,Java仍然保持標明類型。類型名稱現在包括List(讀作:“List-of-String”)和Map, CachedObject>(“Map-of-Class-of-Unknown-Type-to-CachedObject”),但這些仍然是命名的類型,並且每個非基本類型的值仍是某個類的實例。

Java 6和7引入的特性

Java 6基本上是一個性能優化和類庫增強的版本。類型系統的唯一變化是擴大注解角色,發布可插拔注解處理功能。這對大多數開發者沒有任何影響,Java 6中也沒有真正提供可插拔類型系統。

Java 7的類型系統沒有重大改變。僅有的一些新特性,看起來都很相似:

javac編譯器中類型推斷的小改進。

簽名多態性分派(Signature polymorphic dispatch),用於方法句柄(Method handle)的實現細節,而這在Java 8中又反過來用於實現Lambda表達式。

Multi-catch提供了一些“代數數據類型”的小跟蹤信息,但這些完全是javac內部的,對最終用戶程序員沒有任何影響。

Java 8的類型系統

縱觀其歷史,Java基本上已經由其類型系統所定義。它是語言的核心,並且嚴格遵守著標明類型。從實際情況來看,Java類型系統在Java 5和7之間沒有太大變化。

乍一看,我們可能期望Java 8改變這種狀況。畢竟,一個簡單的Lambda表達式似乎讓我們移除了標明類型:

() -> { System.out.println("Hello World!"); }

這是個沒有名字、沒有參數的方法,返回void。它仍然是完全靜態類型的,但現在是匿名的。

我們逃脫了名詞的王國?這真的是Java的一種新的類型形式?

也許不幸的是,答案是否定的。JVM上運行的Java和其它語言,非常嚴格地限制在類的概念中。類加載是Java平台的安全和驗證模式的中心。簡單地說,不通過類來表示一種類型,這是非常非常難的。

Java 8沒有創建新的類型,而是通過編譯器將Lambda表達式自動轉換成一個類的實例。這個類由類型推斷來決定。例如:

Runnable r = () -> { System.out.println("Hello World!"); };

右側的Lambda表達式是個有效的Java 8的值,但其類型是根據左側值推斷的,因此它實際上是Runnable類型的值。需要注意的是,如果沒有正確地使用Lambda表達式,可能會導致編譯器錯誤。即使是引入了Lambda,Java也沒有改變這一點,仍然遵守著標明類型。

Java 8的函數式編程怎麼樣?

最後,讓我們回到本文開頭提出的問題,“Java 8的函數式編程怎麼樣?”

Java 8之前,如果開發者想以函數式風格編程,他或她只能使用嵌套類型(通常是匿名內部類)作為函數代碼的替代。默認的Collection類庫不會為這些代碼提供任何方便,可變性的魔咒也始終存在。

Java 8的Lambda表達式沒有神奇地轉變成函數式語言。相反,它的作用仍是創建強制的強命名類型語言,但有更好的語法支持Lambda表達式函數文本。與此同時,Collection類庫也得到了增強,允許Java開發人員開始采用簡單的函數式風格(例如filter和map)簡化笨重的代碼。

Java 8需要引入一些新的類型來表示函數管道的基本構造塊,如java.util.function中的Predicate、Function和Consumer接口。這些新增的功能使Java 8能夠“稍微函數式編程”,但Java需要用類型來表示它們(並且它們位於工具類包,而不是語言核心),這說明標明類型仍然束縛著Java語言,它離純粹的Lisp方言或者其它函數式語言是多麼的遙遠。

除了以上這些,這個函數式語言能量的小集合很可能是所有大多數開發者日常開發所真正需要的。對於高級用戶,還有(JVM或其它平台)其它語言,並且毫無疑問,將繼續蓬勃發展。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved