1. 程式人生 > >Java8 新特性 —— 函數語言程式設計

Java8 新特性 —— 函數語言程式設計


> 本文部分摘錄自 On Java 8
## 概述 通常,傳遞給方法的資料不同,結果也不同。同樣的,如果我們希望方法被呼叫時的行為不同,該怎麼做呢?結論是:只要能將程式碼傳遞給方法,那麼就可以控制方法的行為。 說得再具體點,過去我們總是建立包含所需行為的物件,然後將物件傳遞給想要控制的方法,一般使用匿名內部類來實現。假設現在有這麼一個需求:有一個員工資訊列表,根據年齡過濾出符合條件的員工資訊 ```java // 過濾出大於35歲的員工 public List filterEmployee(List list) { List emps = new ArrayList<>(); for(Employee emp : list) { if(emp.getAge() > 35) { emps.add(emp); } } return emps; } // 過濾出大於45歲的員工 public List filterEmployee2(List list) { ... } ``` 這樣寫當然能實現需求,但如果需求變了,要過濾 45 歲的,那豈不是又得寫一個 filterEmplyee2() 方法?如果還要過濾 50 歲的,60 歲的,那就沒完沒了了,而且程式碼的實現邏輯幾乎沒有區別。於是我們藉助策略模式的思想來簡化程式碼。 ```java public interface MyPredicate<> { boolean predicate(T t); } // 如果有其他過濾需求,只需要實現 MyPredicate 介面即可 public class EmployeeFilter implements MyPredicate { @Override public boolean predicate(Employee employee) { return t.getAge() >= 35; } } // 根據傳入的 MyPredicate 物件來實現不同的過濾邏輯 public List filterEmployee(List list, MyPredicate mp) { List emps = new ArrayList<>(); for(Employee emp : list) { if(mp.predicate(emp)) { emps.add(emp); } } return emps; } public void test(List list) { // 建立實現類物件,傳入過濾方法 MyPredicate predicate = new EmployeeFilter<>(); List res = filterEmployee(list, predicate); // 更簡單的方式是使用匿名內部類 List res2 = filterEmployee(list, new MyPredicate() { @Override public boolean predicate(Employee employee) { return t.getAge() >= 100; } }); } ``` 通過觀察我們發現,我們需要的只有 predicate() 方法的程式碼,其他的我們一律不關心。如果 MyPredicate 介面還有其他抽象方法,我們又必須每一個做一次實現,但真正用上的只有 predicate() 方法,不僅顯得冗餘,而且可讀性也很低。為了解決這個問題,Java8 為我們提供了 Lambda 表示式和方法引用兩種更加簡潔的方式。
## Lambda 表示式 Lambda 表示式是一個匿名函式,可以把 Lambda 表示式理解為是一段可以傳遞的程式碼(將程式碼像資料一樣傳遞)。雖然在 JVM 規範規定一切都是類,但其幕後執行的各種操作使得 Lambda 看起來像是函式。因此我們可以大膽假設 Lambda 表示式產生的就是一個函式,而不是類。 Lambda 的基本語法有是:`(引數) -> {方法體}` - 其中 `->` 可以視為將引數傳遞給方法體使用的一箇中間橋樑 - 左側為表示式的引數列表。使用括號包裹引數,當只有一個引數時,可以不需要括號,如果沒有引數,則必須使用括號表示空引數列表。引數列表的資料型別可以省略不寫,因為 Java 的編譯器可以幫助我們根據上下文推斷資料型別 - 右側為表示式中所需執行的功能。方法體如果只有單行,可以省略花括號,此時執行結果自動轉化為 Lambda 表示式的放回值,使用 return 關鍵字是非法的;如果方法體有多行,則必須放在花括號中,這時如果有返回值,就需要使用 return Lambda 表示式能產生比匿名內部類更易讀的程式碼,因此我們應該儘可能使用 Lambda 表示式。回到之前的例子,我們可以用 Lambda 表示式來替換匿名內部類。 ```java public interface MyPredicate<> { boolean predicate(T t); } // 根據傳入的 MyPredicate 物件來實現不同的過濾邏輯 public List filterEmployee(List list, MyPredicate mp) { List emps = new ArrayList<>(); for(Employee emp : list) { if(mp.predicate(emp)) { emps.add(emp); } } return emps; } public void test(List list) { // 使用 Lambda 表示式 List res = filterEmployee(list, e -> e.getAge() <= 5000); } ``` Lambad 表示式通常比匿名內部類產生更易讀的程式碼,因此我們應該儘可能使用 Lambda 表示式。 如果我們想編寫遞迴的 Lambda 表示式,必須注意:
## 方法引用 Lambda 表示式可以幫助我們實現僅呼叫方法,而不做其他多餘動作(如建立物件)的目的,而有些情況下,已經存在能滿足需求的方法,我們可以不必再編寫 Lambda 表示式,而通過方法引用直接使用該方法。可以理解為方法引用是 Lambda 表示式的另一種表現形式。 方法引用的組成:類名或物件名,後面跟 `::`,然後跟方法名稱,如果要分類的話,可以用如下組合: - 引用靜態方法 `className::staticMethod` - 引用某個物件的例項方法 `instance::instanceMethod` - 引用某個型別的任意物件的例項方法 `className::instanceMethod` - 引用構造方法 `className::new` ```java interface Callable { void call(String s); } class Describe { void show(String msg) { System.out.println(msg); } } public class MethodReferences { static void hello(String name) { System.out.println("Hello, " + name); } public static void main(String[] args) { // 物件名:: 方法名稱 Describe d = new Describe(); Callable c = d::show; c.call("call()"); // 類名::方法名 c = MethodReferences::hello; c.call("Bob"); } } ``` 要注意的是,方法引用的簽名(引數型別和返回型別)必須符合 Callable 的 call() 的簽名。上述程式碼我沒有演示 `className::instanceMethod` 和 `className::new` 的情況,這兩個有點特殊,待會再介紹。
## Runnable 介面 通過之前的學習,我們發現 Runnable 介面也符合特殊的單方法介面格式:它的 `run()` 方法不帶引數,也沒有返回值,因此我們可以使用 Lambda 表示式和方法引用作為 Runnable ```java class Go { static void go() { System.out.println("thread go"); } } public class RunnableMethodReference { public static void main(String[] args) { // 匿名內部類方式 new Thread(new Runnable() { public void run() { System.out.println("Anonymous"); } }).start(); // Lambda 表示式方式 new Thread( () -> System.out.println("lambda") ).start(); // 方法引用方式 new Thread(Go::go).start(); } } ```
## 未繫結的方法引用 未繫結的方法引用是指沒有關聯物件的普通(非靜態方法),使用未繫結的引用,我們必須先提供物件 ```java class X { String f() { return "X::f()"; } } interface MakeString { String make(); } interface TransformX { String transform(X x); } public class UnboundMethodReference { public static void main(String[] args) { // MakeString ms = X::f; // 無法通過編譯 TransformX sp = X::f; X x = new X(); System.out.println(sp.transform(x)); System.out.println(x.f()); // 同等效果 } } ``` 我們看到在 `MakeString ms = X::f;` 中,即使 `make()` 和 `f()` 有相同的方法簽名,卻無法通過編譯。這是因為實際上還有另一個隱藏引數 `this` 沒有考慮,你不能在沒有 `X` 物件的情況下呼叫 `f()`,因為它尚未繫結到物件。 要解決這個問題,我們需要一個 `X` 物件,所以我們的介面需要一個額外的引數如 `TransformX`,用來接收一個 `X` 物件。同樣的,在呼叫 `transform(X x)` 方法時,也必須傳遞一個 `X` 物件作為引數。如果你的方法有多個引數,就以第一個引數接受 `this` 的模式來處理。
## 建構函式引用 還可以捕獲建構函式的引用,然後通過引用去呼叫該建構函式。 ```java class Dog { String name; int age; Dog() { name = "stray"; } Dog(String nm) { name = nm; } Dog(String nm, int yrs) { name = nm; age = yrs; } } interface MakeNoArgs { Dog make(); } interface Make1Arg { Dog make(String name); } interface Make2Args { Dog make(String name, int age); } public class CtorReference { public static void main(String[] args) { MakeNoArgs mna = Dog::new; Make1Arg m1a = Dog::new; Make2Args m2a = Dog::new; Dog dn = mna.make(); Dog d1 = m1a.make("Comet"); Dog d2 = m2a.make("Ralph", 4); } } ```
## 函式式介面 介面中只有一個抽象方法的介面,稱為函式式介面,可以使用註解 `@FunctionalInterface` 檢查一個介面是否符合函式式介面的規範。 Lambda 表示式和方法引用都要賦值給對應的函式式介面引用。Java8 提供了一組 `java.util.function` 包,它包含一組完整的函式式介面,一般情況下,我們可以直接使用,而不需要自己再定義。 Java 為我們提供了內建的四大核心函式式介面: - 消費型介面 有引數,無返回值型別的介面 ```java @FunctionalInterface public interface Consumer { void accept(T t); } ``` - 供給型介面 只有產出,沒有輸入,就是隻有返回值,沒有入參 ```java @FunctionalInterface public interface Supplier { T get(); } ``` - 函式型介面 既有入參,也有返回值,T 表示函式的引數型別,R 表示函式的返回型別 ```java @FunctionalInterface public interface Function { R apply(T t); } ``` - 斷言型介面 輸入一個引數,返回一個 boolean 型別的返回值 ```java @FunctionalInterface public interface Predicate { boolean test(T t); } ``` 除了上述的四個核心內建介面,Java 還為我們提供其他常用的函式式介面,如 `BiFunction` 也是函式型介面,但可以接收兩個引數,我們可以根據需要去查閱 API 文件。
## 函式組合 意為多個組合成新的函式,一些 `java.util.function` 介面包含支援函式組合的方法 - `andThen(Function after)` 返回一個組合函式,前一個函式的結果作為後一個函式的入參 - `compose(Function before)` 返回一個組合函式,後一個函式首先處理原始入參,再將結果交給前一個函式處理 - `and(Predicate other)` 返回一個組合的謂詞,表示該謂詞與另一個謂詞的短路邏輯與 - `or(Predicate other)` 返回一個組合的謂詞,表示該謂詞與另一個謂詞的短路邏輯或 - `negate()` 返回表示此謂詞的邏輯否定的謂詞
## 閉包 考慮一個函式,x 是 其中的一個入參,i 則是其中的一個區域性變數,返回一個 Lambda 表示式 ```java public class Closure { IntSupplier makeFun(int x) { int i = 0; return () -> x + i; } } ``` 我們知道,函式的入參的區域性變數只在方法的生命週期內有效,正常情況下,當 `makeFun(int x)` 方法執行完後,x 和 i 就會消失,但它返回的 Lambda 表示式卻依然儲存著 x 和 i 的值。相當於 `makeFun(int x)` 返回的 `IntSupplier` 關住了 x 和 i 另外要注意的一點是:被 Lambda 表示式引用的區域性變數必須是 final 或是等同 final 效果的。所謂等同 final,意思是即使你沒有明確宣告變數是 final,但因變數值沒被改變過而實際上有了 final 同等的效果。Java8 預設 Lambda 中的區域性變數具有等同 final 效果。
## 柯里化 柯里化意為:將一個多引數的函式,轉換為一系列單引數函式 ```java public class CurryingAndPartials { // 未柯里化 static String uncurried(String a, String b) { return a + b; } public static void main(String[] args) { // 柯里化的函式 // a -> b -> a + b,意思是傳入引數 a,返回 b -> a + b 的函式 // 由於 Lambda 表示式的閉包特性,b -> a + b 中的 a 是有儲存值的 Function> sum = a -> b -> a + b; System.out.println(uncurried("Hi ", "Ho")); Function hi = sum.apply("Hi "); System.out.println(hi.apply("Ho")); Function sumHi = sum.apply("Hup "); System.out.println(sumHi.apply("Ho")); System.out.println(sumHi.apply("Hey")); } } ``` 柯里化的目的是通過提供一個引數來建立一個新函式,根據上述的例子,我們可以通過新增級別來柯里化具有更多引數的函