關於「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或其它平台)其它语言,并且毫无疑问,将继续蓬勃发展。