儘管目前不少公司已經使用 Java8 做爲項目開發語言,可是仍然有一部分開發者只是將其設置到 pom 文件中,並未真正開始使用。而項目中若是有8新特性的寫法,例如λ表達式。也只是 Idea Alt+Enter 生成的。最近天氣很是熱,出門曬太陽不如和我一塊兒系統的學習一下 Java8 的新特性。提升開發效率也可、享受同事羨慕的眼神也可,讓咱們開始吧java
聲明:本文首發於博客園,做者:後青春期的Keats;地址:https://www.cnblogs.com/keatsCoder/ 轉載請註明,謝謝!程序員
函數式編程:Lambda表達式、流式編程數據庫
其餘特性:默認方法、新的Optional類、CompletableFutrue、LocalDate/LocalTimeexpress
這篇文章重點討論 Lambda 及某些狀況下更易讀、更天然的:方法引用。編程
行爲參數化就是一個方法接受多個不一樣的行爲做爲參數,並在內部使用他們,完成不一樣行爲的能力。其實說白了就是將一段代碼做爲另外一個方法的形參,使該方法更加的靈活、能夠應對多變的需求。設計模式
例如老師安排張三這麼一個任務("法外狂徒"張三改行作程序員了):籃子有不少蘋果 List
這個需求很簡單,張三兩下就搞定了:ide
public static List<Apple> filterGreenApples(List<Apple> appleList){ List<Apple> result = new ArrayList<>(); for (Apple apple : appleList) { if("green".equals(apple.getColor())){ result.add(apple); } } return result; }
但是這個時候老師改主意了。說綠色的很差吃想吃紅色的蘋果,張三隻好複製這個方法進行修改,將green
改爲red
並修改方法名爲 filterRedApples。然而若是老師又讓他篩選多種其餘顏色的蘋果,例如:淺綠色、暗紅色、黃色等。這種複製、修改的方法就顯得有些難應付。一個良好的原則是嘗試抽象其共性。函數式編程
對於篩選蘋果的需求,能夠嘗試給方法添加一個參數 color。很是簡單的就能夠應對老師對不一樣顏色蘋果的需求。函數
public static List<Apple> filterApplesByColor(List<Apple> appleList, String color){ List<Apple> result = new ArrayList<>(); for (Apple apple : appleList) { if(color.equals(apple.getColor())){ result.add(apple); } } return result; }
張三滿意的提交了代碼。可是這時老師又對張三說:我想要一些重一點的蘋果,通常大於150g的蘋果就是比較重的。做爲程序員,張三早就想好老師可能會改重量。所以提早定義一個參數做爲蘋果的重量:
public static List<Apple> filterApplesByWeight(List<Apple> appleList, int weight){ List<Apple> result = new ArrayList<>(); for (Apple apple : appleList) { if(apple.getWeight() > weight){ result.add(apple); } } return result; }
解決方案不錯。但是張三複制了大量的方法用於遍歷庫存。並對每一個蘋果應用篩選條件。他打破了DRY(Dont repeat youselt 不要重複本身)的軟件設計原則。試想一下,若是張三想換一種遍歷的方式,那麼每一個方法都須要再改一次,工做量很大。那有沒有一種方法能將顏色和質量組合成一個方法呢?能夠嘗試加一個 flag,而後根據 flag 的值來肯定使用哪一個判斷條件。但這種方法十分差勁!試想若是之後有了更多的條件:蘋果的大小、產地、品種等等。這個代碼應該怎麼維護?所以張三須要一種更加靈活的方式來實現篩選蘋果的方法。
無論使用什麼條件篩選,他們都有共性:
其中執行一段代碼這一步是不肯定的,而參數和返回值是肯定的,所以咱們能夠定義一個接口:
public interface ApplePredicate { boolean test(Apple apple); }
及不一樣條件篩選的實現:
public class AppleHeavyWeightPredicate implements ApplePredicate{ @Override public boolean test(Apple apple) { return apple.getWeight() > 150; } }
篩選的方法也能夠改爲這樣:
public static List<Apple> filterApples(List<Apple> appleList, ApplePredicate applePredicate){ List<Apple> result = new ArrayList<>(); for (Apple apple : appleList) { if(applePredicate.test(apple)){ result.add(apple); } } return result; }
這時不管應對怎樣的需求,張三隻須要從新實現 test 方法,而後經過 filterApples 方法傳遞 test 方法的行爲。這表示 filterApples 方法的行爲參數化了!
可是張三又以爲這樣的實現太麻煩了,每次新來一個需求他都須要建立一個類實現 ApplePredicate 接口。有沒有更好的辦法呢?答案是確定的。在 Java8 以前能夠經過匿名類來實現:
public static void main(String[] args) { List<Apple> appleList = new ArrayList<>(); appleList.add(new Apple("red", 150)); List<Apple> result = filterApples(appleList, new ApplePredicate() { @Override public boolean test(Apple apple) { return "green".equals(apple.getColor()) && apple.getWeight() > 150; } }); }
匿名類雖然能夠解決建立新類的問題,可是他太長了。那要如何簡化呢? Java8 提供的 Lambda 就是專門用來簡化它的。且看代碼:
public static void main(String[] args) { List<Apple> appleList = new ArrayList<>(); appleList.add(new Apple("red", 150)); List<Apple> result = filterApples(appleList, apple -> "green".equals(apple.getColor()) && apple.getWeight() > 150); }
從蘋果的例子能夠看到,行爲參數化是一種頗有用的模式,它可以輕鬆應對多變的需求,它經過把一個行爲(一段代碼)封裝起來,並經過傳遞和使用建立的行爲將其參數化。這種作法相似於策略設計模式。而JavaAPI中已經在多出實踐過這個模式了,例如 Comparator 排序、Runnable執行代碼塊等等
Lambda是一種簡潔的傳遞一個行爲的匿名函數,它沒有名稱,卻有參數列表、函數主體、返回值、甚至還能夠拋出異常。基本語法像這樣:
(parameters) -> {statements;}
或
(parameters) -> expression
函數式接口就是隻定義一個抽象方法的接口(若是接口中定義了默認方法實現,不管有多少個。只要它只有一個抽象方法,它仍然是函數式接口)
前面咱們在 ApplePredicate 接口中只定義了一個抽象方法 test,因此 ApplePredicate 接口就是函數式接口。相似的還有 Comparator
public static void main(String[] args) { Runnable r1 = () -> System.out.println("Hello World 1"); Runnable r2 = new Runnable() { @Override public void run() { System.out.println("Hello World 2"); } }; process(r1); process(r2); process(() -> System.out.println("Hello World 3")); } public static void process(Runnable r){ r.run(); }
@FunctionalInterface
該註解能夠用來聲明一個接口是函數式接口,若是接口上有聲明,但程序員又爲接口寫了其餘抽象方法,編譯器會報錯
資源處理(處理文件、數據庫)常見的操做方法就是:打開一個資源、作一些處理、關閉/釋放資源。這個打開和關閉階段老是很類似,而且會圍繞執行處理的哪些重要代碼。這就是所謂的環繞執行模式。例如:
public static String readLine() throws IOException { try(BufferedReader br = new BufferedReader(new FileReader("a.txt"))){ return br.readLine(); } }
這個寫法是有侷限性的,由於你沒法靈活的修改處理邏輯的代碼。那就跟着我來將他改形成 lambda 可用的形式吧
行爲參數化
首先咱們要作的行爲定義爲 processFile。如下是從文件中讀取兩行的參數化寫法
String result = processFile( BufferedReader br -> br.readLine() + br.readLine())
使用函數式接口來傳遞行爲
processFile 這個方法須要匹配的函數描述符長這樣: BufferedReader -> String 。那咱們能夠照着它定義接口
@FunctionalInterface public interface BufferedReaderProcesser { String profess(BufferedReader br); }
執行一個行爲
改造 processFile 方法,讓 BufferedReaderProcesser 接口做爲它所執行行爲的載體
public static String processFile(BufferedReaderProcesser brf) throws IOException { try(BufferedReader br = new BufferedReader(new FileReader("a.txt"))){ return brf.professFile(br); } }
傳遞Lambda
接下來就可使用 Lambda 來傳遞不一樣的行爲來以不一樣的方式處理文件了:
public static void main(String[] args) throws IOException { // 讀一行 String str1 = processFile(br -> br.readLine()); // 讀兩行 String str2 = processFile(br -> br.readLine() + " " + br.readLine()); // 找到第一個包含 lambda 的行 String str3 = processFile(br -> { String s; while ((s = br.readLine()).length() > 0) { if (s.contains("lambda")) { return s; } } return null; } ); System.out.println(str1); System.out.println(str2); System.out.println(str3); }
且看控制檯的輸出:
Java8 的設計師們在 java.util.function 包中引入了不少新的函數式接口,如下是幾個經常使用的
@FunctionalInterface public interface Predicate<T> { boolean test(T t); }
布爾類型接口:在須要將一個任意類型的對象處理成布爾表達式時,可能須要它。例如咱們以前處理的蘋果,固然 T 也能夠是學生對象(篩選出身高大於多少的)、用戶對象(篩選具備某特徵的用戶)等等
@FunctionalInterface public interface Consumer<T> { void accept(T t); }
消費類型接口:Consumer 是一個消費型方法,他接收一個泛型
@FunctionalInterface public interface Function<T, R> { R apply(T t); }
若是你須要定義一個 Lambda 將輸入的對象信息映射到輸出,那 Function 是再合適不過的了。
function 包下還有須要相似的函數式接口,讀者能夠自行去關注一下接口中方法的參數和返回值。來決定使用哪一個。
經過上面的介紹,讀者已經對 Lambda 表達式的寫法有了必定的瞭解,那麼 Java 編譯器是如何識別 Lambda 的參數和返回值的呢?
Java 經過上下文(好比,接受他傳遞的方法的參數或是接受他值的局部變量)來推斷 Lambda 表達式須要的目標類型而這個目標類型通常是一個函數式接口,以後判斷表達式的參數和返回值是否與接口中惟一抽象方法的聲明相對應
Java 編譯器從上下文中推斷出表達式的目標類型後,表達式的參數類型也就被編譯器所知道。因此書寫表達式時能夠省略參數類型,例如:
String str1 = processFile(br -> br.readLine());
processFile 方法的參數(Lambda的目標類型)是:BufferedReaderProcesser brf。BufferedReaderProcesser 接口惟一的抽象方法:String profess(BufferedReader br);方法聲明的參數類型是 BufferedReader 。Java 編譯器能夠推斷到這裏。所以直接寫 br 是沒問題的。對於兩個參數的方法也能夠省略參數類型。而一個參數的方法能夠省略參數類型和參數兩邊的括號
方法引用讓你能夠重複使用現有的方法定義,並像 Lambda 同樣傳遞它們。即提早寫好的,可複用的 Lambda 表達式。若是一個 Lambda 表明的只是「直接調用這個方法」,那最好仍是用名稱調用它。方法引用的寫法以下:
目標引用::方法名 // 由於這裏沒有實際調用方法,故方法的 () 不用寫
指向靜態方法的方法引用
(args) -> ClassName.staticMethod(args) 寫成 ClassName::staticMethod
指向任意類型實例方法的方法引用,例如 T 類的實例 arg0
(arg0, rest) -> arg0.instanceMethod(rest) 寫成 T::instanceMethod
指向現有對象的實例方法的方法引用。
(args) -> expr.instanceMethod(args) 寫成 expr::instanceMethod
第二類和第三類乍看有些迷糊,仔細分辨能夠發現:若是方法的調用者是 Lambda 的參數,則目標引用是調用者的類。若是調用者是已經存在的實例對象,則目標引用是該對象
方法引用還能夠被用在構造函數上,寫法是這樣:ClassName::new
好比獲取對於獲取類型Supplier的接口,我分別用三種寫法寫出建立一個蘋果對象的方法:
// 方法引用寫法 Supplier<Apple> s1 = Apple::new; // Lambda 寫法 Supplier<Apple> s2 = () -> new Apple(); // 普通寫法 Supplier<Apple> s3 = new Supplier<Apple>() { @Override public Apple get() { return new Apple(); } };
上面咱們所討論的 Lambda 表達式都是單獨使用的,而 function 包中不少接口中還定義了額外的默認方法,用來複合 Lambda 表達式。
假如咱們有一個給蘋果按指定重量排序的方法
List<Apple> appleList = new ArrayList<>(); // 構造一個按質量升序排序的比較器 Comparator<Apple> c = Comparator.comparing(Apple::getWeight); appleList.sort(c); // 按質量倒敘 appleList.sort(c.reversed());
其中,Comparator.comparing 方法是一個簡化版的 compare 方法的實現形式,源碼以下:
public static <T, U extends Comparable<? super U>> Comparator<T> comparing( Function<? super T, ? extends U> keyExtractor) { Objects.requireNonNull(keyExtractor); return (Comparator<T> & Serializable) (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2)); }
該方法接收一個 Function 接口的實現類做爲參數,而咱們的 Apple::getWeight 方法解析過來就是實現了 Function 接口,重寫 apply 方法,apply 方法的聲明解析爲 int apply(Apple a)
,方法內經過調用 a.getWeight() 方法返回 int 類型的值。後來 return 語句中的 (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
其實就是 Comparator 的 Lambda 表達式實現的匿名類中的方法體。重寫的是 int compare(T o1, T o2);
方法
咱們常常遇到這樣的問題,比較蘋果質量時,質量相同。那麼接下來就須要第二選擇條件了。Comparable 接口也提供了便於 Lambda 使用的比較器鏈方法 thenComparing。好比首先比較質量,當質量相同時按照價格降序
Comparator<Apple> c = Comparator.comparing(Apple::getWeight); Comparator<Apple> compareByWeightThenPrice = c.thenComparing(Apple::getPrice).reversed(); appleList.sort(compareByWeightThenPrice);
Predicate 謂詞接口中有三個可用的複合方法: and、or、negate 分別表示與或非。使用方法和比較器複合大同小異,讀者能夠自行體驗
Function 函數接口中有 andThen() 和 compose() 方法,參數都是 Function 的實現,區別以下
a.andThen(b) 是先執行 a 再執行 b
a.compose(b) 是先執行 b 再執行 a