函數式編程基本概念入門
函數式編程(英語:functional programming)或稱函數程序設計,又稱泛函編程,是一種編程典範,它将電腦(nǎo)運算(suàn)視爲數學上的(de)函數計算(suàn),并且避免使用(yòng)程序狀态以及易變對(duì)象。函數編程語言最重要的(de)基礎是λ演算(suàn)(lambda calculus)。而且λ演算(suàn)的(de)函數可(kě)以接受函數當作輸入(引數)和(hé)輸出(傳出值)。比起指令式編程,函數式編程更加強調程序執行的(de)結果而非執行的(de)過程,倡導利用(yòng)若幹簡單的(de)執行單元讓計算(suàn)結果不斷漸進,逐層推導複雜(zá)的(de)運算(suàn),而不是設計一個(gè)複雜(zá)的(de)執行過程。這(zhè)是維基百科給出的(de)定義。從這(zhè)個(gè)我們知道函數式編程是相對(duì)于指令式編程的(de)一種編程典範,并且對(duì)而言具有一些優點。
特性:
1、函數是"第一等公民"
什(shén)麽是"第一等公民"?所謂"第一等公民"(first class),指的(de)是函數與其他(tā)數據類型一樣,處于平等地位,它不僅擁有一切傳統函數的(de)使用(yòng)方式(聲明(míng)和(hé)調用(yòng)),可(kě)以賦值給其他(tā)變量(賦值),也(yě)可(kě)以作爲參數,傳入另一個(gè)函數(傳參),或者作爲别的(de)函數的(de)返回值(返回)。函數可(kě)以作爲參數進行傳遞,意味我們可(kě)以把行爲"參數化(huà)",處理(lǐ)邏輯可(kě)以從外部傳入,這(zhè)樣程序就可(kě)以設計得(de)更靈活。
2、沒有"副作用(yòng)"
所謂"副作用(yòng)"(side effect),指的(de)是函數内部與外部互動(最典型的(de)情況,就是修改全局變量的(de)值),産生運算(suàn)以外的(de)其他(tā)結果。函數式編程強調沒有"副作用(yòng)",意味著(zhe)函數要保持獨立,所有功能就是返回一個(gè)新的(de)值,沒有其他(tā)行爲,尤其是不得(de)修改外部變量的(de)值。
3、引用(yòng)透明(míng)
引用(yòng)透明(míng)(Referential transparency),指的(de)是函數的(de)運行不依賴于外部變量或"狀态",隻依賴于輸入的(de)參數,任何時(shí)候隻要參數相同,引用(yòng)函數所得(de)到的(de)返回值總是相同的(de)。這(zhè)裏強調了(le)一點"輸入"不變則"輸出"也(yě)不變,就像數學函數裏面的(de)f(x),隻要輸入的(de)x一樣那得(de)到的(de)結果也(yě)肯定定是一樣的(de)。
優點:
1、代碼簡潔,開發快(kuài)速。
函數式編程大(dà)量使用(yòng)函數,減少了(le)代碼的(de)重複,因此程序比較短,開發速度較快(kuài)。Paul Graham在《黑(hēi)客與畫(huà)家》一書(shū)中寫道:同樣功能的(de)程序,極端情況下(xià),Lisp代碼的(de)長(cháng)度可(kě)能是C代碼的(de)二十分(fēn)之一。如果程序員(yuán)每天所寫的(de)代碼行數基本相同,這(zhè)就意味著(zhe),"C語言需要一年時(shí)間完成開發某個(gè)功能,Lisp語言隻需要不到三星期。反過來(lái)說,如果某個(gè)新功能,Lisp語言完成開發需要三個(gè)月(yuè),C語言需要寫五年。"當然,這(zhè)樣的(de)對(duì)比故意誇大(dà)了(le)差異,但是"在一個(gè)高(gāo)度競争的(de)市場(chǎng)中,即使開發速度隻相差兩三倍,也(yě)足以使得(de)你永遠(yuǎn)處在落後的(de)位置。"
2. 接近自然語言,易于理(lǐ)解
函數式編程的(de)自由度很高(gāo),可(kě)以寫出很接近自然語言的(de)代碼。以java爲例把學生以性别分(fēn)組:
沒用(yòng)labmda表達式:
1
2
3
4
5
6
|
Map<String,List<Student>> studentsMap = new HashMap<>();
for(Student student : students){
List<Student> studentList = studentsMap.getOrDefault(student.getSex(), new ArrayList<>());
studentList.add(student);
studentsMap.put(student.getSex(),studentList);
}
|
用(yòng)了(le)lambda表達式:
1
|
Map<String,List<Student>> studentsMap = students.stream().collect(Collectors.groupingBy(Student::getSex));
|
這(zhè)基本就是自然語言的(de)表達了(le),大(dà)家應該一眼就能明(míng)白它的(de)意思吧。
3. 更方便的(de)代碼管理(lǐ)
函數式編程不依賴、也(yě)不會改變外界的(de)狀态,隻要給定輸入參數,返回的(de)結果必定相同。因此,每一個(gè)函數都可(kě)以被看做(zuò)獨立單元,很有利于進行單元測試(unit testing)和(hé)除錯(debugging),以及模塊化(huà)組合。
4. 易于"并發編程"
函數式編程不需要考慮"死鎖"(deadlock),因爲它不修改變量,所以根本不存在"鎖"線程的(de)問題。不必擔心一個(gè)線程的(de)數據,被另一個(gè)線程修改,所以可(kě)以很放心地把工作分(fēn)攤到多(duō)個(gè)線程,部署"并發編程"(concurrency)。
請看下(xià)面的(de)代碼:
var s1 = Op1();
var s2 = Op2();
var s3 = concat(s1, s2);
由于s1和(hé)s2互不幹擾,不會修改變量,誰先執行是無所謂的(de),所以可(kě)以放心地增加線程,把它們分(fēn)配在兩個(gè)線程上完成。其他(tā)類型的(de)語言就做(zuò)不到這(zhè)一點,因爲s1可(kě)能會修改系統狀态,而s2可(kě)能會用(yòng)到這(zhè)些狀态,所以必須保證s2在s1之後運行,自然也(yě)就不能部署到其他(tā)線程上了(le)。多(duō)核CPU是将來(lái)的(de)潮流,所以函數式編程的(de)這(zhè)個(gè)特性非常重要。
5. 代碼的(de)熱(rè)升級
函數式編程沒有副作用(yòng),隻要保證接口不變,内部實現是外部無關的(de)。所以,可(kě)以在運行狀态下(xià)直接升級代碼,不需要重啓,也(yě)不需要停機。Erlang語言早就證明(míng)了(le)這(zhè)一點,它是瑞典愛(ài)立信公司爲了(le)管理(lǐ)電話(huà)系統而開發的(de),電話(huà)系統的(de)升級當然是不能停機的(de)。
缺點:
1、函數式編程常被認爲嚴重耗費在CPU和(hé)存儲器資源。主因有二:
惰性求值亦爲語言如 Haskell增加了(le)額外的(de)管理(lǐ)工作。
2、語言學習(xí)曲線陡峭,難度高(gāo)
函數式語言對(duì)開發者的(de)要求比較高(gāo),學習(xí)曲線比較陡,而且很容易因爲其靈活的(de)語法控制不好程序的(de)結構。
介紹完函數式編程的(de)概念和(hé)優缺點之後,下(xià)面讓我們來(lái)進入java8 lambda的(de)編程世界~
Lambda表達式的(de)組成
java 8 中Lambda 表達式由三個(gè)部分(fēn)組成:第一部分(fēn)爲一個(gè)括号内用(yòng)逗号分(fēn)隔的(de)形式參數,參數是函數式接口裏面方法的(de)參數;第二部分(fēn)爲一個(gè)箭頭符号:->;第三部分(fēn)爲方法體,可(kě)以是表達式和(hé)代碼塊。語法如下(xià)
1、方法體爲表達式,該表達式的(de)值作爲返回值返回。
1
2
|
(parameters) -> expression
(int a,int b) -> return a + b; //求和(hé)
|
2、方法體爲代碼塊,必須用(yòng) {} 來(lái)包裹起來(lái),且需要一個(gè) return 返回值,但若函數式接口裏面方法返回值是 void,則無需返回值。
1
2
3
|
(parameters) -> { statements; }
(int a) -> {System.out.println("a = " + a);} //打印,無返回值
(int a) -> {return a * a;} //求平方
|
Lambda表達式的(de)底層實現
java 8 内部Lambda 表達式的(de)實現方式在本質是以匿名内部類的(de)形式的(de)實現的(de),看下(xià)面代碼。代碼中我們定義了(le)一個(gè)叫binaryOperator的(de)Lambda表達式,看返回值它是一個(gè)IntBinaryOperator實例。
1
2
3
4
5
|
IntBinaryOperator binaryOperator = (int a, int b) -> {
return a + b;
};
int result = binaryOperator.applyAsInt(1, 2);
System.out.println("result = " + result); //3
|
我們再看一下(xià)IntBinaryOperator的(de)定義
1
2
3
4
5
6
7
8
9
10
|
@FunctionalInterface
public interface IntBinaryOperator {
/**
* Applies this operator to the given operands.
* @param left the first operand
* @param right the second operand
* @return the operator result
*/
int applyAsInt(int left, int right);
}
|
我們得(de)知IntBinaryOperator是一個(gè)接口并且上面有一個(gè)@FunctionalInterface的(de)注解,@FunctionalInterface标注了(le)這(zhè)是一個(gè)函數式接口,所以我們知道了(le)(int a, int b) -> {return a + b;}返回的(de)一個(gè)IntBinaryOperator的(de)匿名實現類。
Lambda表達式的(de)函數式接口
上面提到了(le)函數式接口,那這(zhè)是一個(gè)什(shén)麽樣的(de)概念呢(ne)?
函數式接口(Functional Interface)是Java 8對(duì)一類特殊類型的(de)接口的(de)稱呼。這(zhè)類接口隻定義了(le)唯一的(de)抽象方法的(de)接口(除了(le)隐含的(de)Object對(duì)象的(de)公共方法,因此最開始也(yě)就做(zuò)SAM類型的(de)接口(Single Abstract Method)。定義函數式接口的(de)原因是在Java Lambda的(de)實現中,開發組不想再爲Lambda表達式單獨定義一種特殊的(de)Structural函數類型,稱之爲箭頭類型(arrow type,依然想采用(yòng)Java既有的(de)類型(class, interface, method等).原因是增加一個(gè)結構化(huà)的(de)函數類型會增加函數類型的(de)複雜(zá)性,破壞既有的(de)Java類型,并對(duì)成千上萬的(de)Java類庫造成嚴重的(de)影(yǐng)響。權衡利弊,因此最終還(hái)是利用(yòng)SAM 接口作爲 Lambda表達式的(de)目标類型.另外對(duì)于函數式接口來(lái)說@FunctionalInterface并不是必須的(de),隻要接口中隻定義了(le)唯一的(de)抽象方法的(de)接口那它就是一個(gè)實質上的(de)函數式接口,就可(kě)以用(yòng)來(lái)實現Lambda表達式。
在java 8中已經爲我們定義了(le)很多(duō)常用(yòng)的(de)函數式接口它們都放在java.util.function包下(xià)面,一般有以下(xià)常用(yòng)的(de)四大(dà)核心接口:
函數式接口 |
參數類型 |
返回類型 |
用(yòng)途 |
Consumer<T>(消費型接口) |
T |
void |
對(duì)類型爲T的(de)對(duì)象應用(yòng)操作。void accept(T t) |
Supplier<T>(供給型接口) |
無 |
T |
返回類型爲T的(de)對(duì)象。 T get(); |
Function<T, R>(函數型接口) |
T |
R |
對(duì)類型爲T的(de)對(duì)象應用(yòng)操作并返回R類型的(de)對(duì)象。R apply(T t); |
Predicate<T>(斷言型接口) |
T |
boolean |
确定類型爲T的(de)對(duì)象是否滿足約束。boolean test(T t); |
Lambda表達式的(de)應用(yòng)場(chǎng)景
1、使用(yòng)() -> {} 替代匿名類
1
2
3
4
5
6
7
8
|
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("no use lambda");
}
});
Thread t2 = new Thread(() -> System.out.println("use lambda"));
|
我們看到相對(duì)而言Lambda表達式要比匿名類要優雅簡潔很多(duō)~。
2、以流水(shuǐ)線的(de)方式處理(lǐ)數據
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
List<Integer> integers = Arrays.asList(4, 5, 6,1, 2, 3,7, 8,8,9,10);
List<Integer> evens = integers.stream().filter(i -> i % 2 == 0)
.collect(Collectors.toList()); //過濾出偶數列表 [4,6,8,8,10]<br>
List<Integer> sortIntegers = integers.stream().sorted()
.limit(5).collect(Collectors.toList());//排序并且提取出前5個(gè)元素 [1,2,3,4,5]
List<Integer> squareList = integers.stream().map(i -> i * i).collect(Collectors.toList());//轉成平方列表
int sum = integers.stream().mapToInt(Integer::intValue).sum();//求和(hé)
Set<Integer> integersSet = integers.stream().collect(Collectors.toSet());//轉成其它數據結構比如set
Map<Boolean, List<Integer>> listMap = integers.stream().collect(Collectors.groupingBy(i -> i % 2 == 0)); //根據奇偶性分(fēn)組
List<Integer> list = integers.stream().filter(i -> i % 2 == 0).map(i -> i * i).distinct().collect(Collectors.toList());//複合操作
|
借助stream api和(hé)Lambda表達式,以住需要定義多(duō)個(gè)變量,編寫數十行甚至數百行的(de)代碼的(de)集合操作,現在都基本簡化(huà)成了(le)可(kě)以在一行之内完成~
3、更簡單的(de)數據并行處理(lǐ)
1
|
List<Integer> squareList = integers.stream().parallel().map(i -> i * i).collect(Collectors.toList());//轉成平方列表
|
數據并行處理(lǐ),隻需要在原來(lái)的(de)基礎上加一個(gè)parallel()就可(kě)以開啓~。順便提一下(xià)這(zhè)裏parallel()開啓的(de)底層并行框架是fork/join,默認的(de)并行數是Ncpu個(gè)。
4、用(yòng)内部叠代取代外部叠代
外部叠代:描述怎麽幹,代碼裏嵌套2個(gè)以上的(de)for循環的(de)都比較難讀懂(dǒng);隻能順序處理(lǐ)List中的(de)元素;
内部叠代:描述要幹什(shén)麽,而不是怎麽幹;不一定需要順序處理(lǐ)List中的(de)元素
1
2
3
4
5
6
7
8
|
List features = Arrays.asList("Lambdas", "Default Method", "Stream API", "Date and Time API");
for (String feature : features) {
System.out.println(feature); //外部叠代
}
List features = Arrays.asList("Lambdas", "Default Method", "Stream API",
"Date and Time API");
features.stream.forEach(n -> System.out.println(n)); //内部叠代
|
5、重構現有臃腫代碼,更高(gāo)的(de)開發效率
在Lambda表達式出現之前,我們的(de)處理(lǐ)邏輯隻能是以命令式編程的(de)方式來(lái)實現,需要大(dà)量的(de)代碼去編寫程序的(de)每一步操作,定義非常多(duō)的(de)變量,代碼量和(hé)工作量都相對(duì)的(de)巨大(dà)。如果用(yòng)Lambda表達式我們看到以往數十行甚至上百行的(de)代碼都可(kě)以濃縮成幾行甚至一行代碼。這(zhè)樣處理(lǐ)邏輯就會相對(duì)簡單,開發效率可(kě)以得(de)到明(míng)顯提高(gāo),維護工作也(yě)相對(duì)容易。
Lambda表達式中的(de)Stream
在java 8 中 Stream 不是集合元素,它不保存數據,它是有關算(suàn)法和(hé)計算(suàn)的(de),它更像一個(gè)高(gāo)級版本的(de) Iterator。原始版本的(de) Iterator,用(yòng)戶隻能顯式地一個(gè)一個(gè)遍曆元素并對(duì)其執行某些操作;高(gāo)級版本的(de) Stream,用(yòng)戶隻要給出需要對(duì)其包含的(de)元素執行什(shén)麽操作,比如 “過濾掉長(cháng)度大(dà)于 10 的(de)字符串”、“獲取每個(gè)字符串的(de)首字母”等,Stream 會隐式地在内部進行遍曆,做(zuò)出相應的(de)數據轉換。
Stream 就如同一個(gè)叠代器(Iterator),單向,不可(kě)往複,數據隻能遍曆一次,遍曆過一次後即用(yòng)盡了(le),就好比流水(shuǐ)從面前流過,一去不複返。而和(hé)叠代器又不同的(de)是,Stream 可(kě)以并行化(huà)操作,叠代器隻能命令式地、串行化(huà)操作。顧名思義,當使用(yòng)串行方式去遍曆時(shí),每個(gè) item 讀完後再讀下(xià)一個(gè) item。而使用(yòng)并行去遍曆時(shí),數據會被分(fēn)成多(duō)個(gè)段,其中每一個(gè)都在不同的(de)線程中處理(lǐ),然後将結果一起輸出。Stream 的(de)并行操作依賴于 Java7 中引入的(de) Fork/Join 框架(JSR166y)來(lái)拆分(fēn)任務和(hé)加速處理(lǐ)過程。
Stream可(kě)以有限的(de)也(yě)可(kě)以是無限的(de),流的(de)構造方式有很多(duō)可(kě)以從常用(yòng)的(de)Collection(List,Array,Set and so on...),文件,甚至函數....
由值創建流:
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
由數組創建流:
int[] numbers = {2, 3, 5, 7, 11, 13}; int sum = Arrays.stream(numbers).sum();
由文件創建流
Stream<String> lines =Files.lines(Paths.get("data.txt"), Charset.defaultCharset())
上面的(de)這(zhè)些Stream都是有限的(de),我們可(kě)以用(yòng)函數來(lái)創建一個(gè)無限Stream
Stream.iterate(0, n -> n + 2).forEach(System.out::println);
Stream也(yě)很懶惰,它隻會在你真正需要數據的(de)時(shí)候才會把數據給傳給你,在你不需要時(shí)它一個(gè)數據都不會産生。
Lambda表達式的(de)Best Practice
1、保持Lambda表達式簡短和(hé)一目了(le)然
1
2
3
4
5
6
7
8
9
10
11
|
values.stream()
.mapToInt(e -> {
int sum = 0;
for(int i = 1; i <= e; i++) {
if(e % i == 0) {
sum += i;
}
}
return sum;
})
.sum()); //代碼複雜(zá)難懂(dǒng)
|
1
2
3
|
values.stream()
.mapToInt(e -> sumOfFactors(e))
.sum() //代碼簡潔一目了(le)然
|
長(cháng)長(cháng)的(de)Lambda表達式通(tōng)常是危險的(de),因爲代碼越長(cháng)越難以讀懂(dǒng),意圖看起來(lái)也(yě)不明(míng),并且代碼也(yě)難以複用(yòng),測試難度也(yě)大(dà)。
2、使用(yòng)@FunctionalInterface 注解
如果你确定了(le)某個(gè)interface是用(yòng)于Lambda表達式,請一定要加上@FunctionalInterface,表明(míng)你的(de)意圖。不然将來(lái)說不定某個(gè)不知情的(de)家夥比如你旁邊的(de)好基友,在這(zhè)個(gè)interface上面加了(le)另外一個(gè)抽像方法時(shí),你的(de)代碼就悲劇了(le)。
3、優先使用(yòng)java.util.function包下(xià)面的(de)函數式接口
java.util.function 這(zhè)個(gè)包下(xià)面提供了(le)大(dà)量的(de)功能性接口,可(kě)以滿足大(dà)多(duō)數開發人(rén)員(yuán)爲lambda表達式和(hé)方法引用(yòng)提供目标類型的(de)需求。每個(gè)接口都是通(tōng)用(yòng)的(de)和(hé)抽象的(de),使它們易于适應幾乎任何lambda表達式。開發人(rén)員(yuán)應該在創建新的(de)功能接口之前研究這(zhè)個(gè)包,避免重複定義接口。另外一點就是,裏面的(de)接口不會被别人(rén)修改~。
4、不要在Lambda表達中執行有"副作用(yòng)"的(de)操作
"副作用(yòng)"是嚴重違背函數式編程的(de)設計原則,在工作中我經常看到有人(rén)在forEach操作裏面操作外面的(de)某個(gè)List或者設置某個(gè)Map這(zhè)其實是不對(duì)的(de)。
5、不要把Lambda表達式和(hé)匿名内部類同等對(duì)待
雖然我們可(kě)以用(yòng)匿名内部類來(lái)實現Lambda表達式,也(yě)可(kě)以用(yòng)Lambda表達式來(lái)替換内部類,但并不代表這(zhè)兩者是等價的(de)。這(zhè)兩者在某一個(gè)重要概念是不同的(de):this指代的(de)上下(xià)文是不一樣的(de)。當您使用(yòng)内部類時(shí),它将創建一個(gè)新的(de)範圍。通(tōng)過實例化(huà)具有相同名稱的(de)新局部變量,可(kě)以從封閉範圍覆蓋局部變量。您還(hái)可(kě)以在内部類中使用(yòng)這(zhè)個(gè)關鍵字作爲它實例的(de)引用(yòng)。但是,lambda表達式可(kě)以使用(yòng)封閉範圍。您不能在lambda的(de)主體内覆蓋範圍内的(de)變量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
private String value = "Enclosing scope value";
public String scopeExperiment() {
Foo fooIC = new Foo() {
String value = "Inner class value";
@Override
public String method(String string) {
return this.value;
}
};
String resultIC = fooIC.method("");
Foo fooLambda = parameter -> {
String value = "Lambda value";
return this.value;
};
String resultLambda = fooLambda.method("");
return "Results: resultIC = " + resultIC +
", resultLambda = " + resultLambda;
}
|
運行上面這(zhè)段代碼我們将到 resultIC = "Inner class value",resultLambda = "Enclosing scope value"。也(yě)就是說在匿名内部類中this指的(de)是自身的(de)引用(yòng),在Lambda表達式中this指的(de)是外部。
6、多(duō)使用(yòng)方法引用(yòng)
在Lambda表達式中 a -> a.toLowerCase()和(hé)String::toLowerCase都能起到相同的(de)作用(yòng),但兩者相比,後者通(tōng)常可(kě)讀性更高(gāo)并且代碼會簡短。
7、盡量避免在Lambda的(de)方法體中使用(yòng){}代碼塊
優先使用(yòng)
1
2
3
4
5
6
|
Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
String result = "Something " + parameter;
//many lines of code
return result;
}
|
而不是
1
2
3
4
|
Foo foo = parameter -> { String result = "Something " + parameter;
//many lines of code
return result;
};
|
8、不要盲目的(de)開啓并行流
Lambda的(de)并行流雖好,但也(yě)要注意使用(yòng)場(chǎng)景。如果平常的(de)業務處理(lǐ)比如過濾,提取數據,沒有涉及特别大(dà)的(de)數據和(hé)耗時(shí)操作,則真的(de)不需要開啓并行流。我在工作中看到有些人(rén)一個(gè)隻有幾十個(gè)元素的(de)列表的(de)過濾操作也(yě)開啓了(le)并行流,其實這(zhè)樣做(zuò)會更慢(màn)。因爲多(duō)行線程的(de)開啓和(hé)同步這(zhè)些花費的(de)時(shí)間往往比你真實的(de)處理(lǐ)時(shí)間要多(duō)很多(duō)。但一些耗時(shí)的(de)操作比如I/O訪問,DB查詢,遠(yuǎn)程調用(yòng),這(zhè)些如果可(kě)以并行的(de)話(huà),則開啓并行流是可(kě)提升很大(dà)性能的(de)。因爲并行流的(de)底層原理(lǐ)是fork/join,如果你的(de)數據分(fēn)塊不是很好切分(fēn),也(yě)不建議(yì)開啓并行流。舉個(gè)例子ArrayList的(de)Stream可(kě)以開啓并行流,而LinkedList則不建議(yì),因爲LinkedList每次做(zuò)數據切分(fēn)要遍曆整個(gè)鏈表,這(zhè)本身就已經很浪費性能,而ArrayList則不會。
|