前言
儘管目前很多公司已經使用 Java8 作為專案開發語言,但是仍然有一部分開發者只是將其設定到 pom 檔案中,並未真正開始使用。而專案中如果有8新特性的寫法,例如λ表示式。也只是 Idea Alt+Enter 生成的。最近天氣非常熱,出門曬太陽不如和我一起系統的學習一下 Java8 的新特性。提高開發效率也可、享受同事羨慕的眼神也可,讓我們開始吧
宣告:本文首發於部落格園,作者:後青春期的Keats;地址:https://www.cnblogs.com/keatsCoder/ 轉載請註明,謝謝!
新特性
函數語言程式設計:Lambda表示式、流式程式設計
其他特性:預設方法、新的Optional類、CompletableFutrue、LocalDate/LocalTime
這篇文章重點討論 Lambda 及某些情況下更易讀、更自然的:方法引用。
Lambda表示式
行為引數化
行為引數化就是一個方法接受多個不同的行為作為引數,並在內部使用他們,完成不同行為的能力。其實說白了就是將一段程式碼作為另一個方法的形參,使該方法更加的靈活、可以應對多變的需求。
舉個關於蘋果的例子
例如老師安排張三這麼一個任務("法外狂徒"張三改行做程式設計師了):籃子有很多蘋果 List
根據具象篩選蘋果
這個需求很簡單,張三兩下就搞定了:
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 的值來確定使用哪個判斷條件。但這種方法十分差勁!試想如果以後有了更多的條件:蘋果的大小、產地、品種等等。這個程式碼應該怎麼維護?因此張三需要一種更加靈活的方式來實現篩選蘋果的方法。
根據抽象條件篩選
不管使用什麼條件篩選,他們都有共性:
- 需要一個蘋果
- 執行一段程式碼
- 返回一個 boolean 的值
其中執行一段程式碼這一步是不確定的,而引數和返回值是確定的,因此我們可以定義一個介面:
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管中窺豹
Lambda是一種簡潔的傳遞一個行為的匿名函式,它沒有名稱,卻有引數列表、函式主體、返回值、甚至還可以丟擲異常。基本語法像這樣:
(parameters) -> {statements;}
或
(parameters) -> expression
在哪裡及如何使用Lambda
函式式介面
函式式介面就是隻定義一個抽象方法的介面(如果介面中定義了預設方法實現,無論有多少個。只要它只有一個抽象方法,它仍然是函式式介面)
前面我們在 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); }
且看控制檯的輸出:
Java提供的函式式介面
Java8 的設計師們在 java.util.function 包中引入了很多新的函式式介面,以下是幾個常用的
Predicate
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
布林型別介面:在需要將一個任意型別的物件處理成布林表示式時,可能需要它。例如我們之前處理的蘋果,當然 T 也可以是學生物件(篩選出身高大於多少的)、使用者物件(篩選具有某特徵的使用者)等等
Consumer
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
消費型別介面:Consumer 是一個消費型方法,他接收一個泛型
Function
@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 表示式
上面我們所討論的 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
總結
- Lambda 和方法引用本身並不難,理解行為引數化是使用 Lambda 和方法引用的前提
- 函式式介面是僅僅宣告瞭一個抽象方法的介面,只有在接受函式式介面的地方才能使用 Lambda 表示式
- 方法引用可以讓你複用現有的方法實現
- Comparator、Predicate、Function等函式式介面都提供了幾個用來結合 Lambda 表示式的預設方法