第一章
為什么要關心Java 8
使用Stream庫來選擇最佳低級執行機制可以避免使用Synchronized(同步)來編寫代碼,這種代碼不僅容易出錯,并且在多核CPU上的執行成本也很高。
因為多核CPU每個處理器內核都有獨立的高速緩存。加鎖需要這些高速緩存同步運行,而這又需要內核間進行緩慢的緩存一致性協議通信。
Streams的作用不僅僅是把代碼傳遞給方法,它提供了一種新的間接地表達行為參數化的方法。
比如,對于兩個只有幾行代碼不同的方法,只需要把不同的那部分代碼作為參數傳遞進去就可以。
流處理
流是一系列數據項,一次只生成一項。
基于Unix操作流的思想,Java 8在java.util.stream
中添加了一個Stream API;Stream API的很多方法可以鏈接起來形成一個復雜的流水線。
Java 8可以透明的把輸入的不相關部分拿到幾個CPU內核上去分別執行Stream操作流水線,并不需要Thread。
Unix操作流示例:
如cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3
將兩個文件連接起來創建一個流,tr會轉換流中的字符,sort會對流中的行進行排序,而tail -3則給出流的最后三行,這些程序通過管道(|)連接在一起。
用行為參數化把代碼傳遞給方法
Java 8增加了通過API來傳遞代碼的能力,把方法(代碼)作為參數傳遞給另一個方法。
并行與共享的可變數據
要能夠同時對不同的輸入安全地執行,這意味著寫代碼不能訪問共享的可變數據。雖然可以使用Synchronized來打破“不能有共享的可變數據”的規則,但是同步迫使代碼按照順序執行,這與并行處理相悖。
沒有共享的可變數據和將方法和函數(即代碼)傳遞給其他方法的能力是函數式編程范式的基石。
“不能有共享的可變數據”的要求意味著,一個方法要通過它將參數值轉換為結果的方法完全描述的,就像是一個數學函數,沒有可見的副作用。
Java中的函數
Java 可能操作的值: 原始值,對象(嚴格來說是對象的引用)。其他的結構(二等公民)雖然有助于表示值的結構,但它們程序執行期間并不能傳遞。程序之間期間能傳遞的是值(一等公民),解決辦法是用方法來定義類,通過類的實例化產生值,人們又發現, 通過在運行時傳遞方法*能夠將方法變成值來傳遞。
讓方法等概念作為值(一等公民)會讓編程變得容易很多。
方法和Lambda作為一等公民
Java 8的一個新功能是方法引用。
要將方法作為值傳給另一個方法,只需用方法引用::語法(即“把這個方法作為值”)將其傳給另一個方法即可。
這樣做的好處是代碼讀起來更接近問題的陳述,方法生成升為了“一等公民”。
與對象引用傳遞對象類似(對象引用使用new創建),當寫下XXX:MethodName
的時候,就創建了一個方法引用,同樣也可以傳遞它。以前的Java版本只能把方法包裹在FileFilter對象里,然后才能傳遞給別的方法。
除了允許函數成為一等值外,Java 8還體現了更廣義的將函數作為值的思想------Lambda
函數式編程風格,即編寫把函數作為一等值來傳遞的程序
傳遞代碼的例子
假設有一個Apple類,它有一個getColor方法,還有一個變量inventory保存著一個Apples的列表。要選出所有的綠蘋果并返回一個列表。在Java 8之前,可能會寫一個這樣的方法:
public static List<Apple> filterGreenApples(List<Apple> inventory){
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory){
//代碼會僅僅選出綠色蘋果,并加入result中
if ("green".equals(apple.getColor())) {
result.add(apple);
}
}
return result;
}
接下來,又想要選出重的蘋果,比如超過150克的,于是又寫了如下方法:
public static List<Apple> filterGreenApples(List<Apple> inventory){
List<Apple> result = new ArrayList<>();
for (Apple apple: inventory){
//代碼會僅僅選出重量大于蘋果,并加入result中
if (apple.equals(apple.getWeight() > 150 ) {
result.add(apple);
}
}
return result;
}
這兩個方法只有一行不同,Java 8由于可以把條件代碼作為參數傳遞進去,這樣可以避免filter方法出現重復的代碼。于是可以這樣寫:
public static boolean isGreenApple(Apple apple) {
return "green".equals(apple.getColor());
}
public static boolean isHeavyApple(Apple apple) {
return apple.getWeight() > 150;
}
public static List<Apple> filterApples(List<Apple> inventory, Predicate<Apple> p){
List<Apple> result = new ArrayList<>();
for(Apple apple : inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}
注意filterApples的參數: Predicate<Apple> p
,這里是從java.util.function.predicate導入的,它的作用是定義一個泛型接口:
public interface Predicate<T> {
boolean test(T t);
}
通過這樣,可以將方法通過謂語(Predicate)參數p傳遞進filterApples。
要使用 filterApples
的話,可以寫成這樣:
List<Apple> greenApples = filterApples(inventory, FilteringApples::isGreenApple);
或者
List<Apple> heavyApples = filterApples(inventory, FilteringApples::isHeavyApple);
Predicate(謂語)在數學上常用來代表一個類似函數的東西,它接受一個參數值,并返回true或false。
從傳遞方法到Lamda
除了把方法作為值來傳遞以外,Java 8還引入了一套記法(匿名函數或Lambda),于是上面的代碼可以寫成:
List<Apple> greenApples2 = filterApples(inventory,
(Apple a) -> "green".equals(a.getColor()));
或者
List<Apple> heavyApples2 = filterApples(inventory,
(Apple a) -> a.getWeight() > 150);
甚至
List<Apple> weirdApples = filterApples(inventory,
(Apple a) -> a.getWeight() < 80 || "brown".equals(a.getColor()));
通過這種方法,你甚至不需要為只用一次的方法寫定義;代碼更干凈清晰。
Q:什么時候使用方法作為值傳遞,什么時候使用Lambda?
A:要是代碼的長度多于幾行,使用Lambda表示的效果并不是一目了然,這樣還是應該使用方法引用來指向一個方法,而不是使用匿名的Lambda。簡言之,使用哪種方法應該以代碼的清晰度為準繩。
流
通過Stream和Lamdba表達式,可以將之前的篩選代碼改進成如下形式:
import static java.util.stream.Collector.toList;
List<Apple> heavyApples =
inventory.stream().filter((Apple a) -> a.getWeight() > 150).collect(toList());
并行處理:
import static java.util.stream.Collector.toList;
List<Apple> heavyApples =
inventory.parallelstream().filter((Apple a) -> a.getWeight() > 150).collect(toList());
和Collection API相比,StreamAPI處理數據的方式非常不同,用集合需要自己去做迭代的過程。你需要用for-each循環一個個去迭代元素、處理元素,這種數據迭代的方法被稱為外部迭代。而有了Stream API,數據處理完全在庫內部進行,這種迭代思想被稱為內部迭代。
計算集群:用高速網絡連接起來的多臺計算機
Colletion主要是為了存儲和訪問數據,而Stream則主要用于描述對數據的計算。
第二章
通過行為參數化傳遞代碼
行為參數化是可以幫助你處理頻繁變更的需求的一種軟件開發模式。
將代碼塊作為參數傳遞給另一個方法,稍后再去執行它。這樣這個方法就基于那塊代碼被參數化了。打個比方,為了實現行為參數化可能會這樣處理一個集合:
- 可以對列表中的每個元素做 “某件事”
- 可以再列表處理完后做 “另一件事”
- 遇到錯誤時可以做 “另外一件事”
這樣一個方法便可以接受不同的新行為作為參數去執行。
舉個栗子:假設要求你對蘋果的不同屬性做篩選,比如大小、形狀、產地、重量等,寫好多個重復的filter方法或者一個巨大的非常復雜的方法都不是好辦法。為此需要一種更好的方法,來把蘋果的選擇標準告訴filterApples方法。更高層次的抽象這個問題,對選擇標準進行建模:蘋果需要根據Apple的某些屬性來返回一個boolean值,這需要一個返回boolean值的函數,我們把它稱為謂詞。
現在我們定義一個接口來對選擇標準建模:
public interface ApplePredicate(){
boolean test(Apple apple);
}
現在就可以用ApplePredicate的多個實現代表不同的選擇標準了:
static class AppleWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
static class AppleColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}
static class AppleRedAndHeavyPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "red".equals(apple.getColor())
&& apple.getWeight() > 150;
}
}
這些標準可以看做filter方法的不同行為。上面做的這些和“策略設計模式”相關,它讓你定義一族算法,把它們封裝起來,然后運行的時候選擇一個算法,這里的算法族就是applePredicate,不同的策略就是AppleHeavyWeightPredicate和AppleGreenColorPredicate。
下面的問題是,要怎么利用applePredicate的不同實現。需要讓filterApples方法接受applePredicate對象,對Apple做條件測試。這就是行為參數化:讓方法接受多種行為作為參數,并在內部使用,來完成不同的行為。
為此需要給filterApples方法添加一個參數,讓它能接受ApplePredicate對象。這樣在軟件工程上帶來的好處就是:**將filterApples方法迭代集合的邏輯與你要應用到集合中每個也元素的行為(這里是謂詞)區分開了。
由此便可以修改ApplePredicate方法為:
public static List<Apple> filterApples(List<Apple> inventory,
ApplePredicate p){
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}
現在便可以創建不同的ApplePredicate對象,并將它們傳遞給filterApples方法。filterApples方法的行為取決于你通過ApplePredicate對象傳遞的代碼。實現了行為參數化。
雖然已經可以把行為抽象出來讓代碼適應需求的變化了,但是這個過程很啰嗦,因為當要把新的行為傳遞給方法的時候,可能需要聲明很多實現接口的類來實例化好幾個只要實例化一次的類(劃重點,這邊說的是對于只要實例化一次的類)。這樣又啰嗦又費時間。
對于這個問題也有對應的解決辦法,Java有一個機制稱為匿名類,它可以讓你同時聲明和實例化一個類,使代碼更為簡潔。
匿名類
匿名類和Java局部類差不多,但匿名類沒有名字。它允許你同時聲明并實例化一個類(隨用隨建)。
用匿名類實現的ApplePredicate對象:
List<Apple> redApples= filterApples(inventory, new ApplePredicate(){
public boolean test(Apple apple){
return "red".euqls(apple.getColor());
}
});
但是這也并不完全令人滿意。第一,它代碼占用很多行。第二,它用起來容易讓人費解。即使匿名類處理在某種程度上改善了為一個借口聲明好幾個實體類的啰嗦問題,讓仍不能讓人滿意。更好的方式是通過Lambda表達式讓代碼更易讀。
上面的代碼在Java8里可以用Lambda表達式重寫為下面的樣子:
List<Apple> result =
filterApples(inventory, (Apple apple) -> "red".equals(getColor()));
將List類型抽象化
目前的filterApples方法還只適用于Apple。還可以將List類型抽象化,從而超越眼前要處理的問題:
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> inventory, ApplePredicate<T> p){
List<T> result = new ArrayList<>();
for(T e : list ){
if(p.test(e)){
result.add(e);
}
}
return result;
}
現在可以把filter方法用在其他列表上了。
真實的例子
下面將通過兩個例子來鞏固傳遞代碼的思想。
用Comparator來排序
想要根據不同的屬性使用一種方法來表示和使用不同的排序行為來輕松地適應變化的需求。
在Java 8中,List自帶了一個sort方法(也可以使用Collections.sort)。sort的行為可以用java.util.Comparator對象來參數化,它的接口如下:
public interface Comparator<T>{
public int compare(T o1,T o2);
}
因此可以隨時創建Comparator的實現,用sort方法表現出不同的行為。
按照重量升序對庫存排序:
inventory.sort(new Comparator<Apple>(){
public int compare(Apple a1,Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
(這段代碼并沒有告訴你如何同時遍歷列表中的兩個元素,不要糾結遍歷問題,下一章會詳細的講解如何編寫和使用Lambda表達式。)
用Runnable執行代碼塊
線程就像是輕量級的進程:它們自己執行一個代碼塊。多個線程可能會運行不同的代碼。為此需要一種方式來代表要執行的一段代碼。在Java里,可以使用Runnable接口表示一個要執行的代碼塊:
public interface Runnabke{
public void run();
}
可以像下面這樣使用這個接口創建執行不同行為的線程:
Thread t = new Thread(new Runnable()){
public void run(){
System.out.println("Hello world");
}
});
用Lambda表達式的話,看起來會是這樣:
Thread t =new Thread(() -> System.out.println("Hello world"));
第三章
Lambda表達式
利用行為參數化這個概念,就可以編寫更為靈活且可重復使用的代碼。但同時,使用匿名類來表示不同的行為并不令人滿意。Java 8引入了Lambda表達式來解決這個問題。它使你以一種很簡潔的表示一個行為或傳遞代碼。
可以將Lambda表達式理解為簡潔地表示可傳遞的匿名函數的一種方式:它沒有名稱,但它有參數列表、函數主體、返回類型,可能還有一個可以拋出的異常列表。
- 匿名 - 因為它不像普通的方法一樣有一個明確的名稱。
- 函數 - 說它是函數是因為Lambda函數不像方法那樣屬于某個特定的類,但和方法要一樣,Lambda有參數列表、函數主體、返回類型,還可能有可以拋出的異常列表。
- 傳遞 - Lambda表達式可以作為參數傳遞給方法或存儲在變量中。
- 簡潔 - 無需像匿名類那樣寫很多模板代碼
使用Lambda的最終結果就是你的代碼變得更清晰、靈活。打比方,利用Lambda表達式,可以更為簡潔地自定義一個Comparator對象。
對比以下兩段代碼:
Comparator <Apple> byWeight = new Comparator<Apple>(){
public int compare(Apple a1,Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
};
(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
代碼看起來更清晰了。基本上只傳遞了真正需要的代碼(compare方法的主體)。
Lambda表達式由三個部分,如圖 Lambda表達式由參數、箭頭和主體組成 所示
- 參數列表 - 這里它采用了Comparator中compare方法的參數,兩個Apple。
- 箭頭 - 箭頭
->
把參數列表與Lambda主體分隔開。 - Lambda主體 - 比較兩個Apple的重量。表達式就是Lambda的返回值。
Lambda 的基本語法是:
(parameters) -> expression
或 (parameters) -> { statements; }
函數式接口
之前的Predicate<T>就是一個函數式接口,因為Predicate僅僅定義了一個抽象方法:
public interface Predicate<T>{
boolean test(T t);
}
簡而言之,函數式接口就是**只定義一個抽象方法的接口。Lambda表達式允許你直接以內聯的形式為函數式接口的抽象方法提供實現,并把整個表達式作為函數式接口的實例。
函數式接口的抽象方法的簽名
把Lambda付諸實踐:環繞執行模式
要從一個文件中讀取一行所需的模板代碼:
public static String processFile() throws IOException{
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))){
return br.readLine();
}
}
現在這段代碼的局限性在于只能讀文件的第一行,如果想要返回頭兩行,甚至是返回使用最頻繁的詞。這時需要把processFile的行為參數化。
需要一個接收BufferedReader并返回String的Lambda。下面是從BufferedReader中打印兩行的寫法:
String result = processFile(BufferedReader br) -> br.readLine() + br.readLine());
現在需要創建一個能匹配BufferedReader -> String,還可以拋出IOException異常的接口:
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
@FunctionalInterface 是什么?
這個標注用于表示該接口會設計成一個函數式接口。如果用@FunctionalInterface
定義了一個接口,而它卻不是函數式接口的話,編譯器將返回一個提示原因的錯誤。
使用它不是必須的,但是使用它是比較好的做法。
現在就可以把這個接口作為新的processFile方法的參數,在方法主體內,對得到BufferedReaderProcessor對象調用process方法執行處理:
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader("data.txt"))){
return p.process(br);
}
}
現在可以通過傳遞不同的Lambda重用processFile方法,并以不用的方式處理文件了。
處理一行:
String oneLine =
processFile((BufferedReader br) -> br.readLine());
處理兩行:
String twoLines =
processFile((BufferedReader br) -> br.readLine() + br.readLine());
完整代碼如下:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class test{
@FunctionalInterface
public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader("c:/tmp/data.txt"))){
return p.process(br);
}
}
public static void main(String[] args) throws IOException {
String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
System.out.println(oneLine);
System.out.println(twoLines);
}
}
現在已經能成功的利用函數式接口來傳遞Lambda。
使用函數式接口
函數接口定義且只定義了一個抽象方法。函數式接口的抽象方法的簽名稱為函數描述符。因此,為了應用不同的Lambda表達式,需要一套能夠描述常見函數描述符的函數式接口。
Java 8在java.util.function包中加入了許多新的函數式接口,你可以重用它來傳遞多個不同的Lambda。
Predicate
java.util.function.Predicate<T>接口定義了一個名叫test的抽象方法,它接受泛型T對象,并返回一個boolean。在需要表示一個涉及類型T的布爾表達式時,就可以使用這個接口。
public interface Predicate<T> {
boolean test(T t);
}
例如:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){
List<T> results = new ArrayList<>();
for (T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
Consumer
java.util.function.Consumer<T>定義了一個名叫accept的抽象方法,它接受泛型T對象,沒有返回。
例如:
@FunctionalInterface
public interface Comsumer<T> {
void accept(T t);
}
public static <T> void forEach(List<T> list, Predicate<T> c){
for (T i: list){
c.accept(i);
}
}
forEach(
Arrays.asList(1,2,3,4,5),
(Interger i) -> System.ou.println(i)
);
Function
java.util.function.Function<T, R>定義了一個名叫apply的抽象方法,它接受一個泛型T對象,并返回一個泛型R的對象。
下面將創建一個map方法,以將一個String列表映射到包含每個String長度的Interger列表:
@FunctionalInterface
public interface Function<T, R> {
R accept(T t);
}
public static <T, R> List<R> map(List<T> list, Function<T, R> f){
List<R> result = new ArrayList<>();
for (T s: list){
result.add(f.apply(s));
}
return result;
}
List<Integer> l =map(Arrays.asList("lambdas","in","action"), (String s) -> s.length());
原始類型特化
Java的類型有兩種: 引用類型 和 原始類型 。但是泛型只能綁定到引用類型。這是由于泛型內部的實現方式造成的。因此Java里有一個將原始類型轉換為對應的引用類型的機制。這個機制叫裝箱(boxing)。相反的操作便叫拆箱(unboxing)。裝箱和拆箱操作是可以由自動裝箱機制來自動完成的。但是這在性能方面要付出代價,裝箱后的值需要更多的內存并且需要額外的內存。
Java 8為原始類型帶來了一個專門的版本,用于在輸入和輸出都是原始類型時避免自動裝箱的操作:
public interface IntPredicate{
boolean test(int t);
}
無裝箱:
IntPredicate evenNumbers = (int i) -> i%2 ==0;
evenNumbers.test(1000);
裝箱:
Predicate<Integer> oddNumbers = (Integer i) -> i%2 == 1;
oddNumbers.test(1000);
類型檢查、類型推斷以及限制
Lambda本身并不包含它在實現哪個函數式接口的信息。為了全面了解Lambda表達式應該知道Lambda的實際類型是什么。
類型檢查
Lambda的類型是從使用Lambda的上下文推斷出來的。上下文(例如接受它傳遞的方法的參數,或接受它的值的局部變量)中Lambda表達式需要的類型稱為目標類型。
同樣的Lambda,不同的函數式接口
有了目標類型的概念,同一個Lambda表達式就可以與不同的函數式接口聯系起來。
類型推斷
可以進一步簡化你的代碼。編譯器會從上下文(目標類型)推斷出痛什么函數式接口來配合Lambda表達式,這意味著它也可以推斷出適合Lambda的簽名,這樣就可以再Lambda語法中省去標注參數類型。
沒有自動類型推斷:
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
有自動類型推斷:
Comparator<Apple> c =(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
使用局部變量
之前介紹的所有Lambda表達式都只用到了其主體里面的參數。但Lambda表達式也允許使用自由變量(不是參數,而是在外層作用域中定義的變量),就像匿名類一樣。它們被稱作捕獲Lambda。
例如:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
Lambda捕獲了portNumber變量。但關于對這些變量可以做什么有一些限制。Lambda可以沒有限制的捕獲實例變量和靜態變量(也就是在其主體中引用)。但是局部變量必須顯示聲明為final(或事實上是final)。
下面的代碼無法編譯,因為portNumber變量被賦值了兩次:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
實例變量和局部變量背后的實現有一個關鍵的不同。實例變量都存儲在堆中,而局部變量則保存在棧上。Java在訪問自由局部變量時,實際上是在訪問它的副本,而不是訪問原始變量。如果Lambda可以直接訪問局部變量,而且Lambda是在一個線程中使用的,則使用Lambda的線程,可能會在分配該變量的線程將這個變量收回之后去訪問該變量,這回引發造成線程不安全的新的可能性。
方法引用
方法引用可以重復使用現有的方法定義,并像Lambda一樣傳遞它們。
下面是用方法引用寫的一個排序的例子:
先前:
invenstory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
之后(使用方法引用和 java.util.Comparator.comparing
):
inventory.sort(comparing(Apple::getWeight));
方法引用可以被看做僅僅調用特定方法的Lambda的一種快捷寫法。它的基本思想是,如果一個Lambda代表的只是“直接調用這個方法”,那最好還是用名稱來調用它,而不是去描述如何調用它。
方法引用就是讓你根據已有的方法實現來創建Lambda表達式。
當你需要使用方法引用時,目標引用放在分隔符 ::
前,方法的名稱放在后面。例如 Apple::getWeight
就是引用了Apple類中定義的方法getWeight。
方法引用也可以看做針對僅僅設計單一方法的Lambda的語法糖。
構建方法引用
方法引用主要有三類。
- 指向靜態方法的方法引用(Integer::parseInt)
- 指向任意類型實例方法的方法引用(String::length)
- 指向現有對象的實例方法的方法引用(Transaction::getValue)
第二種方法引用的思想就是你在引用一個對象的方法,而這個對象本身是Lambda的一個參數。
例如 (String s) -> s.toUpperCase()
可以寫作 String::toUpperCase
。
第三種方法引用指的是,你在Lambda中調用一個已經存在的外部對象的方法。例如,Lambda表達式 () -> expenssiveTransaction.getValue()
可以寫作 expensiveTransaction::getValue
。
方法引用不需要括號,是因為沒有實際調用這個方法。
構造函數引用
可以利用現有構造函數的名稱和關鍵字來創建它的一個引用 ClassName:new
例如:
List<Integer> weight = Arrays.asList(7,3,4,10);
List<Apple> apples = map(weights, Apple::new);
public static List<Apple> map(List<Integer> List, Function<Integer, Apple> f){
List<Apple> result = new ArrayList<>();
for(Integer e: list){
result.add(f.apply(e))
}
return result;
}
Lambda和方法引用實戰(用不同的排序策略給一個Apple列表排序)
第1步:傳遞代碼
Java API已經提供了一個List可用的sort方法,要如何把排序策略傳遞給sort呢?sort方法的簽名樣子如下:
void sort(Comparator<? super E> c)
它需要一個Comparator對象來比較兩個Apple。
第一個解決方案看上去是這樣的:
public class AppleComparator implements Comparator<Apple>{
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
第2步:使用匿名類改進
inventory.sort(new AppleComparator<Apple>(){
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
使用匿名類的意義僅僅在于不用為了只實例化一次而實現一個Comparator。
第3步:使用Lambda表達式
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
或
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
函數復合
還可以把Function接口所代表的Lambda表達式復合起來。Function接口有兩個默認方法:andThen和 compose。它們都會返回Function的一個實例。
- andThen 方法會返回一個函數,它先對輸入應用一個給定函數,再對輸出應用另一個函數。
比如函數f給數字加1,另一個函數給數字乘2:
Function<Integer,Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x * 2;
Function<Integer,Integer> h = f.andThen(g);
int result = h.apply(1);
在數學上意味著g(f(x))。
- compose 方法先把給定的函數用作compose的參數里面給的那個函數,然后再把函數本身用于結果。
Function<Integer,Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x * 2;
Function<Integer,Integer> h = f.compose(g);
int result = h.apply(1);
在數學上意味著f(g(x))。
第四章
引入流
集合是Java中使用最多的API。幾乎每個Java應用程序都會制造和處理集合。但集合的操作卻遠遠算不上完美。
流是Java API,它允許你以聲明性方式處理數據集合。此外流還可以透明性地并行處理,無需寫任何多線程代碼。
下面是一個Java 7實現的 返回低熱量的菜肴名稱并按照卡路里排序:
List<Dish> lowCaloricDishes = new ArrayList<>();
for(Dish d: dishes){
if(d.getCalories() < 400){
lowCaloricDishes.add(d);
}
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
public int compare(Dish d1, Dish d2){
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish d: lowCaloricDishes){
lowCaloricDishesName.add(d.getName());
}
變量lowCaloricDishes唯一的作用就是作為一次性的中間容器。
下面是Java 8實現:
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName =
menu.stream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dish::getCalories))
.map(Dish::getName)
.collect(toList());
為了利用多核架構并行執行這段代碼,只需要把 stream()
換成 parallelStream()
:
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName =
menu.parallelStream()
.filter(d -> d.getCalories() < 400)
.sorted(comparing(Dish::getCalories))
.map(Dish::getName)
.collect(toList());
使用新的方法 有幾個顯而易見的好處:
- 代碼時以聲明性方式寫的
- 通過把幾個基礎操作鏈接起來,來表達復雜的數據處理流水線,同時保持代碼清晰可讀。
使用Java 8 的Stream API的優點:
- 聲明性
- 可復合
- 可并行
流簡介
流是什么?簡短的定義就是“從支持數據處理操作的源 生成的 元素序列”。
- 元素序列 ----- 就像集合一樣,流也提供一個接口,可以訪問特定元素類型的一組有序值。
- 源 ----- 流會使用一個提供數據的源,如集合、數組或輸入/輸出資源。
- 數據處理操作 ----- 流的數據處理功能支持類似于數據庫的操作。以及函數式編程語言中的常用操作。
此外,流操作有兩個重要的特點:
- 流水線 ----- 很多流操作本身會返回一個流,這樣多個操作就可以連接起來,形成一個大的流水線。
- 內部迭代 ----- 與使用迭代器顯式迭代的集合不同,流的迭代操作是在背后進行的。
例如:
import static java.util.stream.Collectors.toList;
List<String> threeHighCaloricDishesName =
menu.stream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.limit(3)
.collect(toList());
System.out.println(threeHighCaloricDishNames);
在上面示例代碼中,先是對menu調用stream方法,由菜單得到一個流。數據源是menu,它給流提供一個元素序列。接下來,對流應用一系列數據處理操作:filter、map、limit和collect。除了collect之外,所有這些操作都會返回一個流,這樣就可以連接成一條流水線。最后,collect操作開始處理流水線,并返回結果(它和別的操作不一樣,因為它返回的是一個List)。
在調用collect之前,沒有任何結果產生,實際上根本就沒有從menu里選擇元素,可以理解為:鏈中的方法調用都在排隊等待,直到調用collect。
- filter ----- 接受Lambda,從流中排除某些元素。
- map ----- 接受一個Lambda,將元素轉換成其他形式或提取信息。
- limit ----- 截斷流,使其元素不超過給定數量。
- collect ----- 將流轉換為其他形式。
這樣做的好處在于,你并沒有去實現篩選、提取或截斷功能,Stream庫已經自帶了。
流與集合
粗略的說,集合與流之間的差異就在于什么時候進行計算。集合是一個內存中的數據結構,它包含數據結構中目前所有的值,集合中每個元素都得算出來來才能添加到集合中(不管往集合里加東西或者刪東西,集合中的每個元素都是放在內存里的,元素都得先算出來才能成為集合的一部分)。
流則是概念上固定的數據結構,其元素是按需計算的。從另一個角度來說,流就像是一個延遲創建的集合:只有在消費者要求的時候才會計算值。而集合則是急切創建的。
只能遍歷一次
和迭代器類似,流只能遍歷一次。遍歷完以后,這個流就已經被消費掉了。可以從原始數據源那里再獲得一個新的流來重新遍歷一遍。
以下代碼會拋出一個異常,提示流已被消費掉了:
List<String> title = Arrays.asList("Java8","In","Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);
外部迭代和內部迭代
集合和流的另一個關鍵區別在于它們遍歷數據的方式。
使用Collection接口需要用戶去做迭代(比如用for-each),這稱為外部迭代。而Stream庫使用內部迭代 ----- 它幫你把迭代做了,還把得到的流值存在了某個地方,只要給出一個函數說要干什么就可以了。
用for-each循環外部迭代:
List<String> names = new ArrayList<>();
for(Dish d: menu){
names.add(d.getName());
}
用背后的迭代器做外部迭代:
List<String> names = new ArrayList<>();
Iterator<String> iterator =menu.iterator();
while(iterator.hasNext()) {
Dish d = iterator.next();
names.add(d.getName());
}
流:內部迭代:
List <String> names = menu.stream()
.map(Dish::getName)
.collect(toList());
流操作
java.util.stream.Stream中的stream接口定義了許多操作。它們可以被分為兩大類:中間操作和終端操作。可以被連接起來的流操作稱為中間操作,關閉流的操作稱為終端操作。
中間操作
中間操作會返回另一個流。這讓多個操作可以連接起來形成一個查詢。更重要的是,除非流水線上觸發一個終端操作,否則中間操作不會執行任何處理。中間操作會合并起來在終端操作時一次性全部處理。
終端操作
終端操作會從流的流水線生成結果。其結果可以是任何不是流的值。
使用流
流的使用一般包括三件事:
- 一個數據源來執行一個查詢;
- 一個中間操作鏈,形成一條流的流水線;
- 一個終端操作,執行流水線并生成結果。
流的流水線背后的理念類似于構建器模式。在構建器模式中有一個調用鏈用來設置一套配置(流的中間操作鏈),接著是調用built方法(流的終端操作)。
第五章
篩選和切片
用謂詞篩選
Stream接口支持filter方法。該操作會接受一個謂詞作為參數,并返回一個包括所有符合謂詞的元素的流。
例如:
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)
.collect(toList());
篩選各異的元素
流支持distinct方法,它會返回一個元素各異的流。例如,以下代碼會篩選出列表中所有的偶數,并確保沒有重復。
List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);
numbers.stream()
.filter(i -> i%2 ==0)
.distinct()
.forEach(System.out::println);
截短流
流支持limit(n)方法,該方法會返回一個不超過給定長度n的流。
例如:
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
collect(toList());
跳過元素
流還支持skip(n)方法,返回一個扔掉前n個元素的流。如果流元素不足n個則返回一個空流。
例如:
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
collect(toList());
映射
一個常見的數據處理套路就是從某些對象中選擇信息。在SQL里,可以從表中選擇一列。Stream API也通過map和flatMap方法提供了類似的工具。
對流中每一個元素應用函數
流支持map方法,它會接受一個函數作為參數。這個函數會被應用到每個元素上,并將其映射成一個新的元素。
例如:
//提取菜肴名稱
List<String> dishNames = menu.stream()
.map(Dish::getName)
.collect(toList());
流的扁平化
對于一個單詞表,如何返回一張列表,列出里面各不相同的字符?例如給定單詞列表["Hello","World"],要返回列表["H","e","l","o","W","r","d"]。
最開始的版本可能是這樣的:
words.stream()
.map(word -> word.split(""))
.collect(toList());
這樣做的問題在于,傳遞給map方法的Lambda為每個單詞返回了一個String[]。因此map返回的流實際上是Stream<String[]>類型的。而真正想要的是用Stream<String>來表示一個字符流。
- 嘗試使用map和Arrays.stream()
首先,需要一個字符流,而不是數組流。有一個叫做Arrays.stream()的方法可以接受一個數組并產生一個流,例如
String[] arraysOfWords = {Goodbye", "World"};
Stream<String> streamOfWords = Arrays.stream(arrayOfWords);
將它用在前面的流水線里
words.stream()
.map(word -> word.split(""))
.map(Arrays::stream)
.distinct()
.collect(toList());
當前的解決方案仍然搞不定,因為現在得到的是一個流的列表(因為先是把每個單詞轉換成一個字母數組,然后把每個數組變成了獨立的流)
2.使用flatMap
可以像下面這樣使用flatMap來解決這個問題:
List<String> uniqueCharacters =
words.stream()
.map(word -> word.split(""))
.map(Arrays::stream)
.distinct()
.collect(toList());
使用flatMap方法的效果是,各個數組并不是分別映射成一個流,而是映射成流的內容。所有使用map()時生成的單個流都被合并起來,即扁平化為一個流。
簡而言之flatMap方法讓你把一個流中的每個值都換成另一個流,然后把所有的流連接起來稱為一個流。
測驗:
1.給定兩個數字列表[1,2,3]和[3,4],返回綜合能被3整除的數對。
List<Integer> numbers1 = Arrays.asList(1,2,3);
List<Integer> numbers2 = Arrays.asList(3,4);
List<int[]> pairs = numbers1.stream()
.flatmap(i -> numbers2.stream()
.filter( j -> (j+i) % 3 == 0)
.map( j -> new int[]{i,j})
)
.collect(toList());
查找和匹配
另一個常見的數據處理套路是看數據集中某些元素是否匹配一個給定的屬性。Stream API通過allMatch、anyMatch、noneMatch、findFirst和findAny方法提供這樣的工具。
檢查謂詞是否至少匹配一個元素(anyMatch)
if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("The menu is vegetarian friendly");
}
anyMatch方法返回一個boolean值,因此是一個終端操作
檢查謂詞是否匹配所有元素(allMatch)
menu.stream().allMatch( d -> d.getCalories() < 1000);
檢查謂詞是否與所有謂詞都不匹配(noneMatch)
menu.stream().noneMatch( d -> d.getCalories() >= 1000);
查找元素
findAny方法將返回當前流的任意元素:
Optional<Dish> dish =
menu.stream()
.filter(Dish::isVegetarian)
.findAny();
但代碼里的Optional是什么?
Optional簡介
Optional<T>類(java.util.Optional)是一個容器類,代表一個值存在或不存在。Java 8通過引入Optional<T> 來避免返回眾所周知的容易出問題的null。
Optional里有幾種 可以迫使你顯式地檢查值是否存在或處理值不存在情形的 方法。
- isPresent()將在Optional包含值的時候返回true,否則返回false
- isPresent(Consumer<T> block) 會在值存在的時候執行給定的代碼塊。
- T get() 會在值存在時返回值,否則拋出一個NoSuchElement異常。
- T orElse(T other)會在值存在時返回值,否則返回一個默認值。
例如在前面的findAny代碼中你需要顯式地檢查Optional對象中是否存在一道菜可以訪問其名稱:
menu.stream()
.filter(Dish::isVegetarian)
.findAny()
.ifPresent(d -> System.out.println(d.getName());
查找一個元素
有些流有一個出現順序來指定流中項目出現的邏輯順序。對于這種流,想要找到第一個元素。為此有一個findFirst方法,它的工作方式類似于findAny,它們的區別在于并行上的限制,如果不關心返回的元素是哪個,就用findAny。
歸約
如何把一個流中的元素組合起來并表達更復雜的查詢?如“計算菜單中的總卡路里”或“菜單中卡路里最高的菜是哪一個”。此類查詢需要將流中所有元素結合起來,得到一個值,比如一個Integer。這樣的查詢可以被歸類為歸約操作。
元素求和
先看看如何使用for-each循環來對數字列表中的元素求和:
int sum = 0;
for(int x: numbers){
sum += x;
}
這段代碼中有兩個參數:
- 總和變量的初始值
- 將列表中所有元素結合在一起的操作
reduce對上面這種重復應用的模式做了抽象,可以像下面這樣對流中所有的元素求和:
int sum = numbers.stream().reduce(0,(a, b) -> a+b);
reduce接受兩個參數:
- 一個初始值
- 一個BinaryOperator<T>來將兩個元素結合起來產生一個新值。
還可以使用方法引用讓這段代碼更簡潔。在Java 8中,Integer類現在有了 一個靜態的sum方法來對兩個數求和:
int sum = numbers.stream().reduce(0, Integer::sum);
無初始值
reduce還有一個重載的變體,它不接受初始值,但是會返回一個Optional對象(考慮到流中沒有任何元素,無法返回其和的情況):
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a+b);
最大值和最小值
利用reduce來計算最大值和最小值
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
數值流
Stream API還提供了原始類型流特化,專門支持處理數值流的方法
原始類型流特化
Java 8引入了三個原始類型特化流來解決這個問題:IntStream、DoubleStream和LongStream,分別將流中元素特化為int、long和double,可以避免暗含的裝箱成本。每個接口都帶來了進行常用數值歸約的新方法,比如對數值流求和的sum,找到最大元素的max。
- 映射到數值流
將流轉換為特化版本的常用方法是mapToInt、mapToDouble和mapLong。這些方法和前面的map方法的工作方式一樣,只不過返回的是一個特化流,而不是Stream<T>。例如可以像下面這一行用mapToInt對menu中的卡路里求和:
int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
mapToInt會返回一個IntStream,然后就可以調用IntStream接口中定義的sum方法對卡路里求和。如果流是空的,sum默認返回0。
IntStream還支持其他的方法,如max、min、average等。
- 轉換回對象流
要把原始流轉換成一般流,可以使用boxed方法,如下所示:
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
- 默認值OptionalInt
求和很容易,因為它有一個默認值0。但是如果要計算IntStream的最大元素,默認值0是錯誤的結果。如何區分沒有元素的流和最大值真的是0的流?Optional可以用Integer、String等參考類型來參數化。對于三種原始流特化,也分別有一個Optional原始類型特化版本:OptionalInt、OptionalDouble和OptionalLong。
例如要找到IntStream中的最大元素,可以調用max方法,它會返回一個OptionalInt,如果沒有最大值的話,就可以顯示處理OptionalInt去定義一個默認值了:
OptionalInt maxCalories = menu.stream()\
.mapToInt(Dish::getCalories)
.max();
int max = maxCalories.orElse(1);
數值范圍
比如生成1到100之間的所有數字。Java 8引入了兩個可以用于IntStream和LongStream的靜態方法,幫助生成這種范圍:range和rangeClosed。這兩個方法都是第一個參數接受起始值,第二個參數接受結束值。但range生成的范圍不包含結束值,而rangeClosed包含結束值。
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
.filter(n -> n%2 == 0);
System.out.println(evenNumbers.count());
構建流
接下來將介紹如何從值序列、數組、文件來創建流,甚至由生成函數來創建無限流。
由值創建流
使用靜態方法Stream.of通過顯式值創建一個流。它可以接受任意數量的參數。
例如:
Stream<String> stream = stream.of("Java 8","In","Action");
可以使用empty得到一個空流:
Stream<String> emptyStream = stream.empty();
由數組創建流
可以使用靜態方法Arrays.stream從數組創建一個流。它接受一個數組作為參數。
int[] numbers = {2,3,5,7,11,13};
int sum = Arrays.stream(numbers).sum();
由文件生成流
java.nio.file.Files中的很多靜態方法都會返回一個流。例如,Files.lines方法會返回一個由指定文件中的各行構成的字符串流。
統計一個文件中有多少各不相同的詞:
long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("data.txt"),charset.defaultCharset())){
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split("")))
.distinct()
.count();
}
catch(IOException e){
}
上面的代碼使用Files.lines得到一個流,其中的每個元素都是文件中的一行。然后對line調用split方法將行拆分成單詞。最后把distinct和count方法鏈接起來,統計出各不相同的單詞的個數。
由函數生成流
Stream API提供了兩個靜態方法來從函數生成流:Stream.iterate和Stream.generate。這兩個操作可以創建所謂的無限流(不像從固定集合創建的流那樣有固定大小的流)。由iterate和generate產生的流會用給定的函數按需創建值,因此可以無窮無盡地計算下去。一般來說應該使用limit(n)對無限流加以限制,避免打印無窮多個值。
- 迭代
Stream.iterate(0, n -> n+2)
.limit(10)
.forEach(System,out::println);
iterate接受一個初始值。還有一個一次應用在每個產生的新值上的Lambda(UnaryOperator<T>類型)。此操作將生成一個無限流 ----- 這個流沒有結尾,因為值是按需計算的,因此這個流是無界的。
- 生成
與iterate方法不同的是,generate方法不是依次對每個新生成的值應用函數的。它接受一個Supplier<T>類型的Lambda提供新的值。
Stream.generate(Math:random)
.limit(5)
.forEach(System.out::println)