概述
現(xiàn)在主流的編程范式主要有三種,面向過程、面向?qū)ο蠛秃瘮?shù)式編程。在理論部分,已經(jīng)介紹了前面兩種編程范式。本章再講講剩下的編程范式,函數(shù)式編程。
函數(shù)式編程并非是一個很新的東西,早在 50 年前就已經(jīng)出現(xiàn)。近幾年,函數(shù)式編程越來越被人關(guān)注,出現(xiàn)了很多新的函數(shù)式編程語言,比如 Clogure、Scala、Erlang 等。一些函數(shù)式編程語言也加入了很多特性、語法、類庫類支持函數(shù)式編程,比如 Java、Python、Ruby、JavaScript 等。此外,Google Guava 也有對函數(shù)式編程的增強功能。
函數(shù)式編程因其編程的特性,僅在科學(xué)計算、數(shù)據(jù)處理、統(tǒng)計分析等領(lǐng)域,才能更好地發(fā)揮它的優(yōu)勢,所以個人覺得,它并不能完全替代更加通用的面向?qū)ο缶幊谭妒健5?,作為一種補充,它也有很大存在、發(fā)展和學(xué)習(xí)的意義。
到底什么是函數(shù)式編程?
函數(shù)式編程的英文是 Functional Programming。那到底什么是函數(shù)式編程呢?
函數(shù)式編程也沒有一個嚴(yán)格的官方定義,所以,接下來就從特性上告訴你,什么是函數(shù)式編程。
嚴(yán)格來講,函數(shù)式編程中的 “函數(shù)”,并不是指我們編程語言中的 “函數(shù)” 概念,而是指數(shù)學(xué) “函數(shù)” 或者 “表達式”(比如,y=f(x)
)。不過,在編程實現(xiàn)時,對于數(shù)學(xué) “函數(shù)” 或者 “表達式”,一般習(xí)慣性地將它們設(shè)計成函數(shù)。所以,如果不深究的話,函數(shù)式編程中的 “函數(shù)” 也可以理解為編程語言中的 “函數(shù)”。
每個編程范式都有自己獨特的地方,這就是它們會被抽象出來作為一種范式的原因。面向?qū)ο缶幊痰淖畲筇攸c是:以類、對象作為組織代碼的單元以及它的四大特性。面向過程編程語言的最大特點是:以函數(shù)作為組織代碼的單元,數(shù)據(jù)與方法相分離。
函數(shù)式編程最獨特的地方在于它的編程思想。函數(shù)式編程任務(wù),程序可以用一系列數(shù)學(xué)函數(shù)或表達式的組合來表示。函數(shù)式編程是程序面向數(shù)學(xué)的更底層抽象,將計算過程描述為表達式。不過,這樣說你肯定會有疑問,真的可以把任何程序都表示成一組數(shù)學(xué)表達式嗎?
理論上是可以的。但是,并不是所有的程序都適合這么做。函數(shù)式編程有它自己適合的應(yīng)用場景,比如開篇提到的科學(xué)計算、數(shù)據(jù)處理、數(shù)據(jù)處理、統(tǒng)計分析等。在這些領(lǐng)域,程序往往容易使用數(shù)學(xué)表達式來表示,比起非函數(shù)式編程,實現(xiàn)同樣的功能,函數(shù)式編程可以 用很少的代碼就能搞定。但是,對于強業(yè)務(wù)相關(guān)的大型業(yè)務(wù)系統(tǒng)開發(fā)來說,耗費精力地將它抽象成數(shù)學(xué)表達式,硬要用函數(shù)式編程來實現(xiàn),顯然是自討苦吃。相反,在這種應(yīng)用場景下,面向?qū)ο缶幊谈雍线m,寫出來的代碼更加可讀、可維護。
剛剛講的是函數(shù)式編程的編程思想,如果落實到編程實現(xiàn),函數(shù)式編程跟面向過程編程一樣,也是以函數(shù)作為組織代碼的單元。不過,它跟面向過程編程的區(qū)別在于,它的函數(shù)是無狀態(tài)的。
何為無狀態(tài)?簡單地講,函數(shù)內(nèi)部涉及的變量都是局部變量,不會像面向?qū)ο缶幊棠菢?,共享類成員變量,也不會像面向過程編程那樣,共享全局變量。函數(shù)的執(zhí)行結(jié)果只與入?yún)⒂嘘P(guān),跟其他任何外部變量無關(guān)。同樣的入?yún)ⅲ还茉趺磮?zhí)行,得到的結(jié)果是一樣的。這實際上就是數(shù)學(xué)表達式或數(shù)學(xué)函數(shù)的基本要求。下面舉個簡單的例子來解釋下:
// 有狀態(tài)函數(shù):執(zhí)行結(jié)果依賴b的值是多少,即便入?yún)⑾嗤?,多次?zhí)行函數(shù),函數(shù)的返回值有可能不同,因為b值有可能不同
int b
int increase(int a) {
return a + b;
}
// 無狀態(tài)函數(shù):執(zhí)行結(jié)果不依賴任何外部變量,只要入?yún)⑾嗤?,不管?zhí)行多少次,函數(shù)的返回值就相同
int increase(int a, int b) {
return a + b;
}
不同的編程范式之間并不是截然不同的,總有一些相同的編程規(guī)則。比如,不管是面向過程、面向?qū)ο筮€是函數(shù)式編程,它們都有變量、函數(shù)的概念,最頂層都要有 main 函數(shù)執(zhí)行入口,來組裝編程單元(類、函數(shù)等)。只不過,面向?qū)ο蟮木幊虇卧穷惢驅(qū)ο?,面向過程的編程單元室函數(shù),函數(shù)式編程的編程單元是無狀態(tài)函數(shù)。
Java 對函數(shù)式編程的支持
前面章節(jié)講過,實現(xiàn)面向?qū)ο缶幊滩灰欢ǚ堑檬褂妹嫦驅(qū)ο缶幊陶Z言。同理,實現(xiàn)函數(shù)式編程也不一定非得使用函數(shù)式編程語言?,F(xiàn)在,很多面向?qū)ο缶幊陶Z言,也提供了相應(yīng)的語法、類庫來支持函數(shù)式編程。
接下來,看下 Java 這種面向?qū)ο缶幊陶Z言,對函數(shù)式編程的支持,加深一下你對函數(shù)式編程的理解。下面是一段非常典型地 Java 函數(shù)式編程的代碼。
public class FPDemo {
public static void main(String[] args) {
Optional<Integer> result = Stream.of("f", "a", "hello")
.map(s -> s.length())
.filter(l -> l <= 3)
.max((o1, o2) -> o1 - o2);
System.out.println(result.get()); // 輸出2
}
}
這段代碼的作用是從一組字符串?dāng)?shù)組中,過濾出長度小于等于 3 的字符串,并且求得這其中的最大長度。
如果你不了解 Java 函數(shù)式編程的語法,看了上面的代碼或許會有些懵,主要的原因是 Java 為函數(shù)式編程引入了三個新的語法概念:Stream
類、Lambda
表達式和函數(shù)接口(Function Interface)。
-
Stream
類用來支持通過 “.
” 級聯(lián)多個函數(shù)操作的代碼編寫方式; - 引入
Lambda
表達式的作用是簡化代碼編寫; - 函數(shù)接口的作用是可以把函數(shù)包裹成函數(shù)接口,來實現(xiàn)把函數(shù)當(dāng)做參數(shù)一樣來使用(Java 不像 C 一樣支持函數(shù)指針,可以把函數(shù)當(dāng)做參數(shù)來使用)。
首先,看下 Stream 類
假設(shè)要計算這樣一個表達式:(3-1)*2 + 5
。如果按照普通的函數(shù)調(diào)用方式寫出來,就是下面這個樣子:
add(multiply(subtract(3, 1), 2), 5);
這樣看起來,代碼會比較難理解,換個更易懂的寫法:
subtract(3, 1).multiply(2).add(5);
在 Java 中, “.
” 用來表示某個對象的方法。為了支持上面這種級聯(lián)調(diào)用方式,我們讓每個函數(shù)都返回一個通用的類 型:Stream
類對象。在 Stream
類上的操作有兩種:中間操作和終止操作。中間操作仍返回 Stream
類對象,而終止操作返回的是確定的值結(jié)果。
再看下前面的例子。我們對代碼做了注釋解釋,如下所示。其中,map
、filter
是中間操作,返回 Stream
類對象,可以繼續(xù)級聯(lián)其他操作;max
是終止操作,返回的不是 Stream
類對象,無法繼續(xù)往下級聯(lián)處理了。
public class FPDemo {
public static void main(String[] args) {
Optional<Integer> result = Stream.of("f", "a", "hello") // of返回Stream<String>對象
.map(s -> s.length()) // map返回Stream<Integer>對象
.filter(l -> l <= 3) // filter返回Stream<Integer>對象
.max((o1, o2) -> o1 - o2); // max 終止操作:返回Option<Integer>
System.out.println(result.get()); // 輸出2
}
}
其次,再看下 Lambda 表達式
Java 引入 Lambda
表達式的主要作用是簡化代碼編寫。實際上,我們也可以不用 Lambda
表達式來書寫書中的例子。我們拿其中的 map
函數(shù)來舉例說明下。
// Stream中map的定義:
public interface Stream<T> extends BaseStream<T, Stream<T>> {
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
// 省略其他函數(shù)...
}
// Stream中map的使用方法:
Stream.of("fo", "far", "hello").map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
});
// 用Lambda表達式簡化后的寫法:
Stream.of("fo", "far", "hello").map(s -> s.length());
Lambda
表達式語法不是學(xué)習(xí)的重點,這里只是稍微介紹下。
Lambda
表達式包含三部分:輸入、函數(shù)體、輸出。表示出來就是下面這個 樣子:
(a, b) -> { 語句1; 語句2; ...; return 輸出;} // a,b是輸入?yún)?shù)
實際上,Lambda
表達式的寫法非常靈活。剛剛給出的是標(biāo)準(zhǔn)寫法,還有很多簡化寫法。比如,如果輸入?yún)?shù)只有一個,可以省略 ()
,直接寫成 a ->{...}
;如果沒有入?yún)ⅲ梢灾苯訉⑤斎牒图^都省略只保留函數(shù)體;如果函數(shù)體只有一個語句,可以將 {}
省略掉; 如果函數(shù)沒有返回值, return
語句就可以不用寫了。
如果把之前的例子中的 Lambda
表達式,全部替換為函數(shù)接口的實現(xiàn)方式,就是下面這樣子的。代碼是不是多了很多?
Optional<Integer> result = Stream.of("f", "a", "hello")
.map(s -> s.length())
.filter(l -> l <= 3)
.max((o1, o2) -> o1 - o2);
// 還原為函數(shù)接口的實現(xiàn)方式
Optional<Integer> result2 = Stream.of("fo", "far", "hello")
.map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
})
.filter(new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
return integer <= 3;
}
})
.max(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
最后,看下函數(shù)接口
實際上,上面一段代碼中的 Function
、Predicate
、Comparator
都是函數(shù)接口。我們知道,C 語言支持函數(shù)指針,它可以把函數(shù)直接當(dāng)變量來使用。但是,Java 沒有函數(shù)指針這樣的語法。所以,它通過接口函數(shù),將函數(shù)包裹在接口中,當(dāng)做變量來使用。
實際上,函數(shù)接口就是接口。不過,它有自己特別的地方,那就是要求只包含一個未實現(xiàn)的方法。因為,只有這樣,Lambda
表達式才能明確知道匹配的是哪個接口。如果有兩個為實現(xiàn)的方式,并且接口入?yún)ⅰ⒎祷刂刀家粯?,?Java 在翻譯 Lambda
表達式時,就不知道表達式對應(yīng)哪個方法了。
我們把 Java 提供的 Function
、Predicate
這兩個函數(shù)接口的源碼,摘抄過來貼到了下面,你可以看下。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
以上講的就是 Java 對函數(shù)式編程的語法支持。
Guava 對函數(shù)式編程的增強
如果你是 Google Guava 的設(shè)計者,對于 Java 函數(shù)式編程,Google Guava 還能做些什么呢?
Google Guava 并沒有提供太多函數(shù)式編程的支持,僅封裝了幾個遍歷集合操作的接口,代碼如下所示:
Iterables.transform(Iterable, Function);
Iterables.transform(Iterator, Function);
Collections.transform(Collection, Function);
List.transform(List, Function);
Maps.transform(Map, Function);
Multimaps.transform(Multimap, Function);
...
Iterables.filter(Iterable, Predicate);
Iterators.filter(Iterable, Predicate);
Collections2.filter(Collection, Predicate);
...
從 Google 的 Wiki 中,我們發(fā)現(xiàn),Google 對于函數(shù)式編程的使用還是很謹(jǐn)慎的,認(rèn)為過度使用函數(shù)式編程,會導(dǎo)致代碼的可讀性變差,強調(diào)不濫用。所以,在函數(shù)式編程方面,Google Guava 并沒有提供太多的支持。
之所以對遍歷集合操作做了優(yōu)化,主要是因為函數(shù)式編程一個重要的應(yīng)用場景就是遍歷集合。如果不適用函數(shù)式編程,我們只能 for 循環(huán),一個一個的處理集合中的屬性。使用函數(shù)式編程,可以大大簡化遍歷集合操作的代碼編寫,一行代碼就能搞定,而且在可讀性方面也沒有太大的損失。
總結(jié)
本章,講了一下三大編程范式中的最后一個,函數(shù)式編程。盡管越來越多的編程語言開始支持函數(shù)式編程,但我個人覺得,它只能是其他編程語范式的補充,用在一些特殊的領(lǐng)域發(fā)揮它的特殊作用,沒法完全替代面向?qū)ο?、面向過程編程范式。
關(guān)于什么是函數(shù)式編程,實際上不是很好理解。函數(shù)式編程中的 “函數(shù)”,并不是只我們編程語言中的 “函數(shù)” 概念,而是數(shù)學(xué)中的 “函數(shù)” 或者 “表達式” 概念。函數(shù)式編程認(rèn)為,程序可以用一系列數(shù)學(xué)函數(shù)或者表達式的組合來表示。
具體到編程實現(xiàn),函數(shù)式編程以無狀態(tài)函數(shù)作為組織代碼的單元。函數(shù)的執(zhí)行結(jié)果只與入?yún)⒂嘘P(guān),跟其他任何外部變量無關(guān)。同樣的入?yún)?,不管怎么?zhí)行,得到的結(jié)果都是一樣的。文章來源:http://www.zghlxwxcb.cn/news/detail-855511.html
具體到 Java 語言,它提供了三個語法機制來支持函數(shù)式編程。它們分別是 Stream
類、Lambda
表達式和函數(shù)接口。Google Guava 對函數(shù)式編程的一個重要應(yīng)用場景,遍歷集合,做了優(yōu)化,但并沒有太多的支持,并且強調(diào),不要為了節(jié)省代碼行數(shù),濫用函數(shù)式編程,導(dǎo)致代碼可讀性變差。文章來源地址http://www.zghlxwxcb.cn/news/detail-855511.html
到了這里,關(guān)于設(shè)計模式學(xué)習(xí)筆記 - 開源實戰(zhàn)三(下):借助Google Guava學(xué)習(xí)三大編程范式中的函數(shù)式編程的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!