1. 程式人生 > 程式設計 >Java 8函式語言程式設計

Java 8函式語言程式設計

Java 8函式語言程式設計

一、簡介

1.2 什麼是函式語言程式設計

每個人對函式語言程式設計的理解不近相同。但其核心是:在思考問題時,使用不可變值和函式,函式對一個值進行處理,對映成另外一個值。

二、lambda 表示式

匿名函式寫法:

button.addActionListener(new ActionListener(){
    public void actionPerformed(ActionEvent event){
        System.out.println("button clicked");
    }
}
複製程式碼

使用lambda之後:

button.addActionListener(event -> System.out.println("button clicked"
); 複製程式碼

和使用匿名內部類的另一處不同在於宣告 event 引數的方式。使用匿名內部類時需要顯式地宣告引數型別 ActionEvent event,而在 Lambda 表示式中無需指定型別,程式依然可以編譯。這是因為 javac 根據程式的上下(addActionListener 方法的簽名)在後臺推斷出了引數 event 的型別。這意味著如果引數型別不言而明,則無需顯式指定。

目標型別是指Lambda表示式所在上下文環境的型別。比如,將Lambda表示式賦值給一個區域性變數,或轉遞給一個方法作為引數,區域性變數或方法引數的型別就是Lambda表示式的目標型別。

String name = "張三"
; // name = "lisi"; //;例 2-7 不能多次對name賦值,如果多次對name賦值,這 在lambda中引用name變數這會報錯 Button btn = new Button(); btn.addActionListener(event -> System.out.println(name)); 複製程式碼

表2-1 Java中重要的函式介面

介面 引數 返回型別 示例
Predicate T boolean 這張唱片已經發行了嗎
Consumer T void 輸出一個值
Function<T,R> T R 獲得Artist物件的名字
Supplier None T 工廠方法
UnaryOperator T T 邏輯非
BinaryOperator (T,T) T 求兩個數的乘積(*)

** 重要問題**

  1. ThreadLocal Lambda 表示式。Java 有一個 ThreadLocal 類,作為容器儲存了當前執行緒裡 區域性變數的值。Java 8 為該類新加了一個工廠方法,接受一個 Lambda 表示式,併產生 一個新的 ThreadLocal 物件,而不用使用繼承,語法上更加簡潔。 a. 在 Javadoc 或整合開發環境(IDE)裡找出該方法。 b. DateFormatter 類是非執行緒安全的。使用建構函式建立一個執行緒安全的 DateFormatter 物件,並輸出日期,如“01-Jan-1970”。

三、流

建造者模式 使用一系列操作設定屬性和配置,最後呼叫一個build方法,這時,物件才真正建立。

3.1 從外部迭代 到內部迭代

其實在我們平常寫的程式碼中 ,ForEach迴圈遍歷 其實是一個 外部迭代。 for迴圈其實是一個封裝了迭代的語法糖。
工作原理: 首先呼叫iterator方法,產生一個新的Iterator物件,進而控制整個迭代過程,這就是 外部迭代

例 3-2 使用迭代計算來自倫敦的藝術家人數。

int count = 0;
Iterator<Artist> iterator = allArtists.iterator();
while(iterator.hasNext()) {
    Artist artist = iterator.next();
    if (artist.isFrom("London")) {
        count++;
    }
}
複製程式碼

圖3-1 外部迭代 示意圖

內部迭代

例 3-3 使用內部迭代計算來自倫敦的藝術家人數

logn count = allArtists.stream().filter(artist -> artist.isFrom("London")).count();

複製程式碼

圖 3-2 內部迭代示意圖

3.2 實現機制

例3-3 中,整個過程被分解為兩種更簡單的操作:過濾和計數,看似有化簡為繁之嫌—— 例3-1 中只含一個for 迴圈,兩種操作是否意味著需要兩次迴圈?事實上,類庫設計精妙, 只需對藝術家列表迭代一次。

例 3-4 只過濾,不計數

allArtists.stream().filter(artist -> artist.isFrom("London"));
複製程式碼

這行程式碼並未做什麼實際性的工作,filter只刻畫出了Stream,但沒有產生新的集合。 像filter這樣只描述Stream,最終不產生新集合的方法 叫做 惰性求值方法;而像count這樣最終會從Stream產生值的方法叫做及早求值方法

整個過程和建造者模式有共通之處。建造者模式使用一系列操作設定屬性和配置,最後呼叫一個build方法,這時,物件才被真正建立。

3.3 常用的流操作

3.3.1 collect(toList())

collect(toList()) 方法 由Stream裡的值生成一個列表,是一個及早求值操作。

List<String> collected = Stream.of("a","b","c") .collect(Collectors.toList()); 
複製程式碼

Stream的of方法使用一組初始值生成新的stream。

四、類

4.3 過載解析

總而言之,Lambda表示式作為引數時,其型別由它的目標型別推導得出,推導過程遵循如下規則: 1.如果只有一個可能的目標型別,由相應函式介面裡的引數型別推導得出。 2.如果有多個可能的目標型別,由最具體的型別推導得出。 3.如果有多個可能的目標型別且最具體的型別不明確,則需人為制定型別。

4.4 @FunctionalInterface

和Closeable 和 Comparable 介面不同。為了提高Stream物件可操作性而引入的各種新介面,都需要有Lambda表示式可以實現它。它們存在的意義在於講程式碼塊作為資料打包起來。因此,它們都添加了@FunctionInterface註釋。

4.7

三定律

如果對預設方法的工作原理,特別是在多重繼承(類實現多個介面)下的行為還沒有把握,如下三條簡單的定律可以幫助大家:

1.類勝於介面。如果在繼承鏈中有方法體或抽象的方法說明,那麼就可以忽略介面中定義的方法。
2.子類勝於父類。如果一個介面繼承了另一個介面,且兩個介面都定義了一個預設方法。那麼子類中定義的方法勝出。
3.沒有規則三。如果上面兩條規則不適用,子類那麼需要實現該方法,要麼將方法宣告為抽象方法。
複製程式碼

其中第一條規則是為了讓程式碼向後相容。

4.9 介面的靜態方法

Stream是個介面。Stream.of是介面的靜態方法。這也是Java 8中新增的一個新的語言特性,旨在幫助編寫類庫的開發人員,但對於日常應用程式的開發人員也同樣適用。

Stream和其他幾個子類還包含另外幾個靜態方法。特別是range和iterate方法提高了產生Stream的其他方式。

4.10 Optional

reduce方法的一個重點尚未提及:reduce方法有兩種形式,一種如前邊出現的需要有一個初始值,另一種變式則不需要有初始值。在沒有初始值的情況下,reduce的第一步使用Stream中前兩個元素。有時,reduce操作不存在有意思的初始值,這樣做就是有意義的。此時,reduce方法返回一個Optional物件。

Optional是為核心類庫新設計的一個資料型別,用來替換null值。 開發人員常常使用null值表示值不存在,optional物件能更好的表達這個概念。使用null代表值不存在的最大問題在於NullPointerException。 一旦引用一個儲存null值得變數,程式會立即崩潰。

使用Optional物件有兩個目的:首先,Optional物件鼓勵程式設計師適時檢查變數是否為空,以避免程式碼缺陷;其次,它將一個類的API中可能為空的值檔案化,這比閱讀實現程式碼簡單的多。

Optional的常用方法

of 和ofNullable

of和ofNullable是用於建立Optional物件的,of不能建立null物件,而ofNullable可以。

Optional<String> str = Optional.of("sss");  
//of引數為空,拋nullPointException  
//Optional<String> str1 = Optional.of(null);  
//ofNullable,引數可以為空,為空則返回值為空  
Optional<String> str1 = Optional.ofNullable(null); 
複製程式碼
isPresent和get

isPresent是用來判斷物件是否為空,get獲得該物件

if (str.isPresent()) {  
    System.out.println(str.get());  
}  
if (str1.isPresent()) {  
    System.out.println(str1.get());  
}  
複製程式碼
orElse和orElseGet

orElse和orElseGet使用實現黨Optional為空時,給物件賦值。orElse引數為賦值物件,orElseGet為Supplier函式介面。

//orElse  
System.out.println(str.orElse("There is no value present!"));  
System.out.println(str1.orElse("There is no value present!"));  
  
//orElseGet,設定預設值  
System.out.println(str.orElseGet(() -> "default value"));  
System.out.println(str1.orElseGet(() -> "default value"));  
複製程式碼
orElseThrow

orElseThrow時當存在null時,丟擲異常

try {  
    //orElseThrow  
    str1.orElseThrow(Exception::new);  
} catch (Throwable ex) {  
    //輸出: No value present in the Optional instance  
    System.out.println(ex.getMessage());  
} 
複製程式碼

五、高階集合類和收集器

5.1 方法引用

Lambda表示式有一個常見的用法:Lambda表示式經常呼叫引數。比如想得到藝術家的姓名: Lambda表示式如下; artist -> artist.getName() 這種用法如此普遍。Java8為其提供了一個簡寫語法,叫做方法引用,幫助程式設計師重用已有方法。用方法引用重寫上邊面的Lambda表示式,程式碼如下: Artist::getName 標準語法為:Classname::methodName。 注意:方法名後邊不需要新增括號

建構函式也有同樣的縮寫形式,如果你想使用Lambda表示式建立一個Person物件,可能如下程式碼

(name,age) -> new Person(name,age)
複製程式碼

使用方法引用,上面程式碼可寫為:

Person::new
複製程式碼

也可以用這種方式來建立陣列 String[]::new

5.2 元素順序

5.3 收集器

5.3.1 轉換成其他集合

可以轉換成toList(),toSet() 等等。

例5-5 使用toCollection,用定製的集合收集元素

stream.collect(toCollection(TreeSet::new));
複製程式碼

5.3.2 轉換成值

例5-6 找出成員最多的樂隊

public Optional<Artist> biggestGroup(Stream<Artist> artists){
    Function<Artist,Long> getCount = artist -> artist.getMembers().count();
    return artists.collect(Collectors.maxBy(comparing(getCount)));
}
複製程式碼

minBy,是用來找出最小值的。

例 5-7 找出一組專輯上曲目的平均數

public double averageNumberOfTracks(List<Album> albums){
    return albums.stream().collect(Collectors.averagingInt(album -> album.getTrackList().size()));
}
複製程式碼

第4章 介紹過一些特殊的流,如IntStream,為數值定義了一些額外的方法。事實上,Java 8 也提供了能完成類似功能的收集器。如:averageingInt。可以使用summingInt及其過載方法求和。SummaryStatistics也可以使用summingInt及其組合手機。

5.3.3 資料分塊

收集器partitioningBy,它接受一個流,並將其分成兩部分(如圖所示)。它使用Predicate物件判斷一個元素應該屬於哪個部分,並根據布林值翻一個Map到列表。因此,對於true List中的元素,Predicate返回true;對於其他List中的元素,Predicate返回false。

partitioningBy 收集器

例 5-8 將藝術家組成的流分成樂隊和獨唱歌手兩部分

public Map<Boolean,List<Artist>> bandsAndSolo(Stream<Artist> artists) {
    return artists.collect(partitioningBy(artist -> artist.isSolo()));
}
複製程式碼

** 5-9 使用方法引用將藝術家組成的 Stream 分成樂隊和獨唱歌手兩部分 **

public Map<Boolean,List<Artist>> bandsAndSoloRef(Stream<Artist> artists) {
    return artists.collect(partitioningBy(Artist::isSolo));
}
複製程式碼

可以理解成 Oracle分析函式中 partition by

5.3.4 資料分組

資料分組是一種更自然的分割資料操作,與將資料分成 ture 和 false 兩部分不同,可以使用任意值對資料分組。比如現在有一個由專輯組成的流,可以按專輯當中的主唱對專輯分組。

例 5-10 使用主唱對專輯分組

public Map<Artist,List<Album>> albumsByArtist(Stream<Album> albums) {
    return albums.collect(groupingBy(album -> album.getMainMusician()));
}
複製程式碼

groupingBy 收集器(如圖5-2 所示)接受一個分類函式,用來對資料分組,就像 partitioningBy 一樣,接受一個Predicate 物件將資料分成 ture 和 false 兩部分。我們使用的分類器是一個Function 物件,和 map 操作用到的一樣。

groupingBy 收集器
可以理解成 SQL 中 group by

5.3.5 字串

例 5-12 使用流和收集器格式化藝術家姓名

String result = artists.stream().map(Artist::getName).collect(Collectors.join(",","[","]"));
複製程式碼

這裡使用map操作提取藝術家的姓名,然後使用Collectors.joining收集流中的值,該方法可以方便地從一個流得到一個字串,允許使用者提供分隔符(用以分隔元素)、字首和字尾。

5.3.6 組合收集器

現在來考慮如何計算一個藝術家的專輯數量。

一個簡單的方案是使用前面的方法對專輯先分組後計數

例 5-13 計算每個藝術家專輯數的簡單方式

Map<Artist,List<Album>> albumsByArtist = albums.collect(groupingBy(album -> album.getMainMusician()));

Map<Artist,Integer> numberOfAlbums = new HashMap<>();
for(Entry<Artist,List<Album>> entry : albumsByArtist.entrySet()) {
    numberOfAlbums.put(entry.getKey(),entry.getValue().size());
}
複製程式碼

這種方式看起來簡單,但卻有點雜亂無章。這段程式碼也是命令式的程式碼,不能自動適應並行化操作。

這裡實際上需要另外一個收集器,告訴groupingBy不用為每個藝術家生成一個專輯列表,只需要對專輯技術就可以了。 核心類庫已經提供了一個這樣的收集器:counting。

例 5-14 使用收集器計算每個藝術家的專輯數

public Map<Artist,Long> numberOfAlbums(Stream<Album> albums){
    return albums.collect(Collectors.groupingBy(album -> album.getMainMusician(),counting()));
}
複製程式碼

groupingBy 先將元素分成塊,每塊都與分類函式getMainMusician提供的鍵值想關聯。然後使用下游的另一個收集器手機每塊中的元素,最好將結果對映為一個Map。

例 5-15 使用簡單方式求每個藝術家的專輯名

public Map<Artist,List<String>> nameOfAlbumsDumb(Stream<Album> albums) {
    Map<Artist,List<Album>> albumsByArtist =
        albums.collect(groupingBy(album ->album.getMainMusician()));
        Map<Artist,List<String>> nameOfAlbums = new HashMap<>();
        for(Entry<Artist,List<Album>> entry : albumsByArtist.entrySet()) {
                    nameOfAlbums.put(entry.getKey(),entry.getValue()
                    .stream()
                    .map(Album::getName)
                    .collect(toList()));
    }
    return nameOfAlbums;
}
複製程式碼

例 5-16 使用收集器求每個藝術家的專輯名

    public Map<Artist,List<String>> nameOfAlbums(Stream<Album> albums) {
        return albums.collect(groupingBy(Album::getMainMusician,mapping(Album::getName,toList())));
    }
複製程式碼

這兩個例子中我們都用到了第二個收集器,用以收集最終結果的一個子集,這些收集器叫做 下游收集器。收集器是生成最終結果的一劑配方,下游收集器則是生成部分結果的配方,主收集器中會用到下游收集器。這種組合使用收集器的方式,使得他們在Stream類庫中的作用更加強大。

六、資料並行化

6.1 並行和併發

併發是兩個任務共享時間段,並行則是兩個任務在同一個時間發生,比如執行在多核CPU上。如果一個程式要執行兩個任務,並且只有一個CPU給他們分配了不同的時間片,那麼這是併發,不是並行。 兩者區別如圖:

併發和並行的區別

資料並行化:資料並行化是將資料分成塊,為每塊資料分配單獨的處理單元。 當需要在大量資料上執行同樣的操作時,資料並行化很管用,它將問題分解為可在多塊資料上求解的形式,然後對每塊資料執行運算,最後將各資料塊上得到的結果彙總,從而得到最終結果。

阿姆達爾定律是一個簡單規則,預測了搭載多核處理器的機器提升程式速度的理論最大值。

6.3 並行化流操作

以下方法可以獲得一個擁有並行能力的流。

1.parallel() :Stream物件呼叫。 2.parallelStream() :集合物件呼叫,建立一個並行能力的流。

問題:並行化執行基於流的程式碼是否比序列化執行更快?

6.4 模擬系統

討論***蒙特卡洛模擬法***。蒙特卡洛模擬法會重複相同的模擬很多次,每次模擬都使用隨機生成的種子。每次模擬的結果都被記錄下來,彙總得到一個對系統的全面模擬。蒙特卡洛模擬法被大量用在工程、金融和科學計算領域。

詳細 請看程式碼。

6.5 限制

使用reduce限制1 恆等值

之前呼叫reduce方法,初始值可以為任意值,為了讓其在並行化時能正常工作,初始值必須為組合函式的***恆等值*** 。拿恆等值和其他值做reduce操作時,其他值保持不變。 *eg:*使用reduce操作求和,組合函式(acc,element) -> acc + element,則其初始值必須為0。

使用reduce限制2 結合律。

reduce 操作的另一個限制是組合操作必須符合結合律。這意味著只要序列的值不變,組合操作的順序不重要。

注意API中 : prallel() :並行流 sequential() :序列流。

如果同時呼叫這兩個方法,最後呼叫的起效。預設是序列的。

6.6 效能

影響並行流效能的主要因素有5個,如下:

① 資料大小

輸入資料的大小會影響並行化處理對效能的提升。將問題分解之後並行化處理,再將結果合併會帶來額外的開銷。因此只有資料足夠大,每個資料處理管道花費的時間足夠多時,並行化處理才有意義。

② 源資料結構

每個管道的操作都基於一些初始資料來源,通常是集合。將不同的資料來源分隔相對容易,這裡的開銷硬性了在管道中並行處理資料時到底帶來多少效能上的提升。

③ 裝箱

處理基本型別比處理裝箱型別要快。

④ 核的數量

極端情況下,只有一個核,因此完全沒必要並行化。顯然,擁有的核越多,獲得潛在效能提升的幅度就越大。在實踐中,核的數量不單指你的機器上有多少核,更是指執行時你的機器能使用多少核。這也就是說同時執行的其他程式,或者執行緒關聯性(強制執行緒在某些核或 CPU 上執行)會影響效能。

⑤ 單元處理開銷

必須資料大小,這是一場並行執行花費時間和分解合併操作開銷之間的戰爭。花在流中每個元素身上的時間越長,並行操作帶來的效能提升越明現。

來看一個具體的問題,看看如何分解和合並它。

例 6-6 並行求和

private int addIntegers(List<Integer> values){
    return values.parallelStream().mapToInt(i -> i).sum();
}
複製程式碼

在底層,並行流還是沿用了 fork/join 框架。fork 遞迴式地分解問題,然後每段並行執行,最終由 join 合併結果,返回最後的值。

圖6-2 使用fork join分解合併問題

假設並行流將我們的工作分解開,在一個四核的機器上並行執行。

1.資料被分成四塊
2.如6-6所示,計算工作在每個執行緒裡並行執行。這包括將每個Integer物件對映為int值,然後在每個執行緒裡將1/4的數字相加。理想情況下,我們希望在這裡花的時間越多越好,因為這裡是並行操作的最佳場合
3.然後合併結果。在例6-6中,就是sum操作,但這也可能是reduce、collect或其他終結操作。
複製程式碼

資料結構對效能的影響:

效能好

ArrayList、陣列或IntStream.range,這些資料結構支援隨機讀取,也就是它們能輕而易舉的被任意分解。

效能一般

hashSet、TreeSet這些資料結構不易公平的被分解,但是大多時候是可能的。

效能差

有些資料結構難於分解。比如,可能要花O(N)的時間複雜度來分解問題。其中包括LinkedList,對半分解太難了。還有Streams.iterate和BufferedReader.lines。它們長度未知。因此很難預測改在哪裡分解。