讓我們來說說函數(shù)式編程--Lambda表達式
上周開始讨惩,看了這本書--Java 8 函數(shù)式編程产弹,以下是我的讀書摘抄剑勾。
背景知識:
函數(shù)接口:只有一個抽象方法的接口
方法簽名:由方法名+形參列表構(gòu)成+返回值類型量窘。JVM是并沒有特別明確的將數(shù)據(jù)類型寫出來雇寇,而是提供了特殊的表示法。
惰性求值方法: 只描述Stream蚌铜,不產(chǎn)生實際執(zhí)行的方法
及早求值: 立刻執(zhí)行
函數(shù)的副作用:當調(diào)用函數(shù)時锨侯,除了返回函數(shù)值之外,還對主調(diào)用函數(shù)產(chǎn)生附加的影響(例如修改全局變量或修改參數(shù))
純函數(shù):輸入輸出數(shù)據(jù)流全是顯式的(顯式的含義是:函數(shù)與外界交換數(shù)據(jù)只有一個唯一渠道——參數(shù)和返回值冬殃;函數(shù)從函數(shù)外部接受的所有輸入信息都通過參數(shù)傳遞到該函數(shù)內(nèi)部囚痴;函數(shù)輸出到函數(shù)外部的所有信息都通過返回值傳遞到該函數(shù)外部)
非純函數(shù):與純函數(shù)相反,函數(shù)通過參數(shù)和返回值以外的渠道审葬,和外界進行數(shù)據(jù)交換深滚。比如,讀取全局變量涣觉,修改全局變量痴荐。
阿姆達爾定律:對于固定負載情況下描述并行處理效果的加速比s,S=1/(1-a+a/n)旨枯,其中a為并行計算部分所占比例蹬昌,n為并行處理結(jié)點個數(shù)
正文
Lambda表達式
在思考問題時,使用不可變值和函數(shù)攀隔,函數(shù)對一個值進行處理皂贩,映射成另一個值
Lambda表達式
- 傳入一段代碼塊
- 不需要顯式聲明/指定參數(shù)類型(編譯器可以通過方法簽名來推斷類型)
Lambda表達式引用的局部變量必須是final或既成事實上的final變量(基本類型只能賦值一次栖榨,對象的引用不能改變)
Stream API
意義和創(chuàng)新:遍歷從外部迭代變成了內(nèi)部迭代。把程序扮演Director導演類的部分邏輯和操作提取明刷、轉(zhuǎn)移到了類庫中
實現(xiàn)機制
Stream操作分為惰性求值和及早求值婴栽;形成一個惰性求值的鏈,最后用一個及早求值的操作返回想要的結(jié)果辈末。
整個過程被分解為多個惰性求值操作和一個單個及早求值操作愚争,而這些多個操作只需要遍歷一次。整個過程和建造者模式有共同之處挤聘。建造者模式使用一系列操作來設置屬性并配置轰枝,最后調(diào)用build方法,這時组去,對象才被真正創(chuàng)建鞍陨。
常用的流操作
collect:流生成集合
map:改變流中元素的類型
flatMap:把最初流中的一個元素轉(zhuǎn)成一個流
filter:過濾流中的元素
Max/Min:求極值,返回 Optional (涉及比較器類Comparator从隆,因為比較意味著排序诚撵,排序意味著需要一個排序的指標)
reduce
Lambda表達式正確使用姿勢
1)明確要達成什么轉(zhuǎn)化,而不是說明如何轉(zhuǎn)化
2)寫出的函數(shù)盡量沒有副作用
純函數(shù)的優(yōu)點
- 無狀態(tài)键闺,線程安全寿烟。不需要線程同步。
- 純函數(shù)相互調(diào)用組裝起來的函數(shù)辛燥,還是純函數(shù)筛武。
- 可緩存結(jié)果,原生支持并行(不懂)
I/O API可以看作是一種特殊的全局變量购桑。文件畅铭、屏幕、數(shù)據(jù)庫等輸入輸出結(jié)構(gòu)可以看作是獨立于運行環(huán)境之外的系統(tǒng)外全局變量勃蜘,而不是應用程序自己定義的全局變量。
類庫
裝箱類型在Lambda表達式上的體現(xiàn)
- int整型在內(nèi)存中占用4個字節(jié)假残,Integer在內(nèi)存中占用16字節(jié)缭贡;
- 基本類型和裝箱類型相互轉(zhuǎn)化需要額外的計算開銷
- 在Java中想要一個包含整型值的列表List<int>,實際上得到的卻是一個包含整型對象的列表List<Integer>;
對于需要大量數(shù)值運算的算法來說辉懒,由于上述的原因阳惹,減緩了程序的運行速度
因此,Stream類的某些方法對基本類型和裝箱類型做了區(qū)分眶俩;Java 8對整型莹汤,長整型和雙浮點型做了特殊處理;方法有:mapToLong(),MapToInt();
Lambda表達式的重載
Lambda表達式的類型就是對應的函數(shù)接口類型颠印。遇到重載時調(diào)用準則:
- 優(yōu)先執(zhí)行最具體的類型
- 如果最具體的類型有多個纲岭,則需要認為指定類型
默認方法
Java 8新增一個關(guān)鍵詞default抹竹,在接口中,可以存在由default修飾的默認方法(也就是說止潮,Java 8接口里窃判,不再只能是抽象方法,具體的方法也可以存在即默認方法喇闸;同時袄琳,也允許存在靜態(tài)方法),當子類不實現(xiàn)這個接口的某個抽象方法時燃乍,子類就是使用該默認方法唆樊。
還要注意一點:一個類要實現(xiàn)的接口里,存在默認方法和它父類的方法相同刻蟹,則優(yōu)先選擇父類定義的方法窗轩。
歸納出兩條簡單的定律:
- 類勝于接口。如果在繼承鏈中有方法或抽象方法聲明座咆,那么久可以忽略接口中定義的方法
- 子類勝過父類痢艺。如果一個接口繼承了另一個接口,且兩個接口都定義了一個默認的方法介陶,那么子類中定義的方法勝出堤舒。
Optional
該新的數(shù)據(jù)類型,用來替換null哺呜。Optional對象相當于值的容器舌缤,類似ThreadLocal用法
- 創(chuàng)建Optional對象:Optional.of()
- 獲取值:get();
- 獲取可能存在的空值:empty().
- 判斷Optional對象理是否有值:isPresent();
- orElse(T t),當Optional對象為空時,提供一個備選值t
- orElseGet(Supplier supplier):當Optional對象為空時某残,調(diào)用這個函數(shù)接口
特殊的修飾
@FunctionalInterface
該注釋會強制javac檢查一個接口是否符合函數(shù)接口的標準(一個接口国撵,只有一個抽象方法),如果該注釋添加給一個枚舉類型玻墅,類或另一個注釋介牙,或者接口含不止一個抽象方法,javac就會報錯(編譯的時候)澳厢。重構(gòu)代碼時环础,使用它能很容易發(fā)現(xiàn)問題。
高級集合類
方法引用
本小節(jié)摘自【譯】Java 8的新特性—終極版
方法引用使得開發(fā)者可以直接引用現(xiàn)存的方法剩拢、Java類的構(gòu)造方法或者實例對象线得。方法引用和Lambda表達式配合使用,使得java類的構(gòu)造方法看起來緊湊而簡潔徐伐,沒有很多復雜的模板代碼贯钩。
西門的例子中,Car類是不同方法引用的例子,可以幫助讀者區(qū)分四種類型的方法引用角雷。
public static class Car {
public static Car create( final Supplier< Car > supplier ) {
return supplier.get();
}
public static void collide( final Car car ) {
System.out.println( "Collided " + car.toString() );
}
public void follow( final Car another ) {
System.out.println( "Following the " + another.toString() );
}
public void repair() {
System.out.println( "Repaired " + this.toString() );
}
}
第一種方法引用的類型是構(gòu)造器引用祸穷,語法是Class::new,或者更一般的形式:Class<T>::new谓罗。注意:這個構(gòu)造器沒有參數(shù)粱哼。
final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );
第二種方法引用的類型是靜態(tài)方法引用,語法是Class::static_method檩咱。注意:這個方法接受一個Car類型的參數(shù)揭措。
cars.forEach( Car::collide );
第三種方法引用的類型是某個類的成員方法的引用,語法是Class::method刻蚯,注意绊含,這個方法沒有定義入?yún)ⅲ?/p>
cars.forEach( Car::repair );
第四種方法引用的類型是某個實例對象的成員方法的引用,語法是instance::method炊汹。注意:這個方法接受一個Car類型的參數(shù):
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
數(shù)據(jù)并行化
并發(fā)躬充,并行;數(shù)據(jù)并行化讨便;任務并行化
調(diào)用parallelstream()/parallel(),生成一個并行化流
收集器
Collectors.toList()充甚,Collectors.toSet,Collectors.to
調(diào)試Lambda表達式
對于業(yè)務穩(wěn)定,生命周期較長的產(chǎn)品霸褒,可以引入單元測試伴找。
程序員寫出的程序bug,有兩種
- 實現(xiàn)業(yè)務邏輯的思路錯誤
- 實現(xiàn)業(yè)務邏輯的思路對了废菱,但是在實現(xiàn)過程中出了問題技矮,開發(fā)者設想的過程和真實的情況不同。
軟件產(chǎn)品種殊轴,大部分bug來源于第二種衰倦。我們對單元測試,下一個定義:單元測試是測試一段代碼的行為是否符合預期的方式旁理。我們對每一個方法輸出結(jié)果都存在一個預期樊零,這個預期是我們在實現(xiàn)業(yè)務過程中的一個環(huán)節(jié),這些環(huán)節(jié)全部累加起來韧拒,每個環(huán)節(jié)的結(jié)果都是我們想要的淹接,那么這個業(yè)務我們就能正確的實現(xiàn)了。因此叛溢,單元測試,能檢測出第二種bug劲适;
對重構(gòu)Lamdba表達式的思考
如果存在多個方法楷掉,每個方法的處理過程相同,輸入輸出類型相同,不同的是操作的具體邏輯烹植,則可以把相同的操作提取成新的方法斑鸦,不同的操作通過方法入?yún)眢w現(xiàn)。比如下面兩個方法:
計算音樂家數(shù)量
public long countMusicians(){
return albums.stream()
.mapToLong(albu ->.getMusicians().count)
.sum()
}
計算單曲數(shù)量
public long countTracks(){
return albums.stream()
.mapToLong(albu ->.getTracks().count)
.sum()
}
要怎么重構(gòu)呢草雕?
首先我們定義一個函數(shù)接口巷屿,輸入Album類,輸出一個long型, 引入泛型(Java 8其實已經(jīng)定義了這種輸入輸出的接口了)
public interface ToLongFunction<T>{
long applyAsLong(T value);
}
我們知道墩虹,Lambda表達式返回類型是函數(shù)接口嘱巾,因此上述的兩個方法就可以重構(gòu)成下面的樣子
public long countMusicians(){
return countFeature(ablum -> album.getMusicians().count())
}
public long countTracks(){
return countFeature(ablum -> ablum.getTracks().count())
}
private long countFeature(ToLongFuction<Album> function){
return albums.stream()
.mapToLong(function)
.sum();
}
整個思考過程,和非函數(shù)式編程是類似的诫钓。
對惰性求值方法的調(diào)試
Java 8 提供了一個方法peek()旬昭,舉個例子
Stream.of("one", "two", "three", "four")
.peek(e -> System.out.println("Peeked value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
這樣就能對流的每一個元素處理的先后值的變化進行觀察了