每一個人對函數式編程的理解不近相同。但其核心是:在思考問題時,使用不可變值和函數,函數對一個值進行處理,映射成另一個值。java
匿名函數寫法:程序員
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 | 求兩個數的乘積(*) |
建造者模式 使用一系列操做設置屬性和配置,最後調用一個build方法,這時,對象才真正建立。併發
其實在咱們日常寫的代碼中 ,ForEach循環遍歷 實際上是一個 外部迭代。 for循環實際上是一個封裝了迭代的語法糖。
工做原理: 首先調用iterator方法,產生一個新的Iterator對象,進而控制整個迭代過程,這就是 外部迭代app
例 3-2 使用迭代計算來自倫敦的藝術家人數。框架
int count = 0;
Iterator<Artist> iterator = allArtists.iterator();
while(iterator.hasNext()) {
Artist artist = iterator.next();
if (artist.isFrom("London")) {
count++;
}
}
複製代碼
內部迭代函數式編程
例 3-3 使用內部迭代計算來自倫敦的藝術家人數
logn count = allArtists.stream().filter(artist -> artist.isFrom("London")).count();
複製代碼
例3-3 中,整個過程被分解爲兩種更簡單的操做:過濾和計數,看似有化簡爲繁之嫌—— 例3-1 中只含一個for 循環,兩種操做是否意味着須要兩次循環?事實上,類庫設計精妙, 只需對藝術家列表迭代一次。
例 3-4 只過濾,不計數
allArtists.stream().filter(artist -> artist.isFrom("London"));
複製代碼
這行代碼並未作什麼實際性的工做,filter只刻畫出了Stream,但沒有產生新的集合。 像filter這樣只描述Stream,最終不產生新集合的方法 叫作 惰性求值方法;而像count這樣最終會從Stream產生值的方法叫作及早求值方法
整個過程和建造者模式有共通之處。建造者模式使用一系列操做設置屬性和配置,最後調用一個build方法,這時,對象才被真正建立。
collect(toList()) 方法 由Stream裏的值生成一個列表,是一個及早求值操做。
List<String> collected = Stream.of("a", "b", "c") .collect(Collectors.toList());
複製代碼
Stream的of方法使用一組初始值生成新的stream。
總而言之,Lambda表達式做爲參數時,其類型由它的目標類型推導得出,推導過程遵循以下規則: 1.若是隻有一個可能的目標類型,由相應函數接口裏的參數類型推導得出。 2.若是有多個可能的目標類型,由最具體的類型推導得出。 3.若是有多個可能的目標類型且最具體的類型不明確,則需人爲制定類型。
和Closeable 和 Comparable 接口不一樣。爲了提升Stream對象可操做性而引入的各類新接口,都須要有Lambda表達式能夠實現它。它們存在的意義在於講代碼塊做爲數據打包起來。所以,它們都添加了@FunctionInterface註釋。
若是對默認方法的工做原理,特別是在多重繼承(類實現多個接口)下的行爲尚未把握,以下三條簡單的定律能夠幫助你們:
1.類勝於接口。若是在繼承鏈中有方法體或抽象的方法說明,那麼就能夠忽略接口中定義的方法。
2.子類勝於父類。若是一個接口繼承了另外一個接口,且兩個接口都定義了一個默認方法。那麼子類中定義的方法勝出。
3.沒有規則三。若是上面兩條規則不適用,子類那麼須要實現該方法,要麼將方法聲明爲抽象方法。
複製代碼
其中第一條規則是爲了讓代碼向後兼容。
Stream是個接口。Stream.of是接口的靜態方法。這也是Java 8中添加的一個新的語言特性,旨在幫助編寫類庫的開發人員,但對於平常應用程序的開發人員也一樣適用。
Stream和其餘幾個子類還包含另外幾個靜態方法。特別是range和iterate方法提升了產生Stream的其餘方式。
reduce方法的一個重點還沒有說起:reduce方法有兩種形式,一種如前邊出現的須要有一個初始值,另外一種變式則不須要有初始值。在沒有初始值的狀況下,reduce的第一步使用Stream中前兩個元素。有時,reduce操做不存在有意思的初始值,這樣作就是有意義的。此時,reduce方法返回一個Optional對象。
Optional是爲核心類庫新設計的一個數據類型,用來替換null值。 開發人員經常使用null值表示值不存在,optional對象能更好的表達這個概念。使用null表明值不存在的最大問題在於NullPointerException。 一旦引用一個存儲null值得變量,程序會當即崩潰。
使用Optional對象有兩個目的:首先,Optional對象鼓勵程序員適時檢查變量是否爲空,以免代碼缺陷;其次,它將一個類的API中可能爲空的值文檔化,這比閱讀實現代碼簡單的多。
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得到該對象
if (str.isPresent()) {
System.out.println(str.get());
}
if (str1.isPresent()) {
System.out.println(str1.get());
}
複製代碼
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時當存在null時,拋出異常
try {
//orElseThrow
str1.orElseThrow(Exception::new);
} catch (Throwable ex) {
//輸出: No value present in the Optional instance
System.out.println(ex.getMessage());
}
複製代碼
Lambda表達式有一個常見的用法:Lambda表達式常常調用參數。好比想獲得藝術家的姓名: Lambda表達式以下; artist -> artist.getName() 這種用法如此廣泛。Java8爲其提供了一個簡寫語法,叫作方法引用,幫助程序員重用已有方法。用方法引用重寫上邊面的Lambda表達式,代碼以下: Artist::getName 標準語法爲:Classname::methodName。 注意:方法名後邊不須要添加括號
構造函數也有一樣的縮寫形式,若是你想使用Lambda表達式建立一個Person對象,可能以下代碼
(name,age) -> new Person(name,age)
複製代碼
使用方法引用,上面代碼可寫爲:
Person::new
複製代碼
也能夠用這種方式來建立數組 String[]::new
能夠轉換成toList(),toSet() 等等。
例5-5 使用toCollection,用定製的集合收集元素
stream.collect(toCollection(TreeSet::new));
複製代碼
例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及其組合手機。
收集器partitioningBy,它接受一個流,並將其分紅兩部分(如圖所示)。它使用Predicate對象判斷一個元素應該屬於哪一個部分,並根據布爾值翻一個Map到列表。所以,對於true List中的元素,Predicate返回true;對於其餘List中的元素,Predicate返回false。
例 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
數據分組是一種更天然的分割數據操做,與將數據分紅 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 操做用到的同樣。
例 5-12 使用流和收集器格式化藝術家姓名
String result = artists.stream().map(Artist::getName).collect(Collectors.join(",","[","]"));
複製代碼
這裏使用map操做提取藝術家的姓名,而後使用Collectors.joining收集流中的值,該方法能夠方便地從一個流獲得一個字符串,容許用戶提供分隔符(用以分隔元素)、前綴和後綴。
如今來考慮如何計算一個藝術家的專輯數量。
一個簡單的方案是使用前面的方法對專輯先分組後計數
例 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類庫中的做用更增強大。
併發是兩個任務共享時間段,並行則是兩個任務在同一個時間發生,好比運行在多核CPU上。若是一個程序要運行兩個任務,而且只有一個CPU給他們分配了不一樣的時間片,那麼這是併發,不是並行。 二者區別如圖:
數據並行化:數據並行化是將數據分紅塊,爲每塊數據分配單獨的處理單元。 當須要在大量數據上執行一樣的操做時,數據並行化很管用,它將問題分解爲可在多塊數據上求解的形式,而後對每塊數據執行運算,最後將各數據塊上獲得的結果彙總,從而獲得最終結果。
阿姆達爾定律是一個簡單規則,預測了搭載多核處理器的機器提高程序速度的理論最大值。
如下方法能夠得到一個擁有並行能力的流。
1.parallel() :Stream對象調用。 2.parallelStream() :集合對象調用,建立一個並行能力的流。
問題:並行化運行基於流的代碼是否比串行化運行更快?
討論***蒙特卡洛模擬法***。蒙特卡洛模擬法會重複相同的模擬不少次,每次模擬都使用隨機生成的種子。每次模擬的結果都被記錄下來,彙總獲得一個對系統的全面模擬。蒙特卡洛模擬法被大量用在工程、金融和科學計算領域。
詳細 請看代碼。
以前調用reduce方法,初始值能夠爲任意值,爲了讓其在並行化時能正常工做,初始值必須爲組合函數的***恆等值*** 。拿恆等值和其餘值作reduce操做時,其餘值保持不變。 *eg:*使用reduce操做求和,組合函數(acc,element) -> acc + element,則其初始值必須爲0。
reduce 操做的另外一個限制是組合操做必須符合結合律。這意味着只要序列的值不變,組合操做的順序不重要。
注意API中 : prallel()
:並行流 sequential()
:串行流。
若是同時調用這兩個方法,最後調用的起效。默認是串行的。
輸入數據的大小會影響並行化處理對性能的提高。將問題分解以後並行化處理,再將結果合併會帶來額外的開銷。所以只有數據足夠大,每一個數據處理管道花費的時間足夠多時,並行化處理纔有意義。
每一個管道的操做都基於一些初始數據源,一般是集合。將不一樣的數據源分隔相對容易,這裏的開銷硬性了在管道中並行處理數據時到底帶來多少性能上的提高。
處理基本類型比處理裝箱類型要快。
極端狀況下,只有一個核,所以徹底不必並行化。顯然,擁有的核越多,得到潛在性能提高的幅度就越大。在實踐中,核的數量不單指你的機器上有多少核,更是指運行時你的機器能使用多少核。這也就是說同時運行的其餘進程,或者線程關聯性(強制線程在某些核或 CPU 上運行)會影響性能。
必須數據大小,這是一場並行執行花費時間和分解合併操做開銷之間的戰爭。花在流中每一個元素身上的時間越長,並行操做帶來的性能提高越明現。
來看一個具體的問題,看看如何分解和合並它。
例 6-6 並行求和
private int addIntegers(List<Integer> values){
return values.parallelStream().mapToInt(i -> i).sum();
}
複製代碼
在底層,並行流仍是沿用了 fork/join 框架。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。它們長度未知。所以很難預測改在哪裏分解。