Java函數式編程和lambda表達式(轉自51 ZeroOne01)

*/
public class MethodRefrenceDemo {
public static void main(String[] args) {
// 方法引用,調用打印方法


Consumer consumer = System.out::println;
consumer.accept("接收的數據");
// 靜態方法引用,通過類名即可調用
Consumer consumer2 = Dog::bark;
consumer2.accept(new Dog());
// 實例方法引用,通過對象實例進行引用
Dog dog = new Dog();
IntUnaryOperator function = dog::eat;
System.out.println("還剩下" + function.applyAsInt(2) + "斤");
// 另一種通過實例方法引用的方式,之所以可以這麼幹是因為JDK默認會把當前實例傳入到非靜態方法,參數名為this,參數位置為第一個,所以我們在非靜態方法中才能訪問this,那麼就可以通過BiFunction傳入實例對象進行實例方法的引用
Dog dog2 = new Dog();
BiFunction biFunction = Dog::eat;
System.out.println("還剩下" + biFunction.apply(dog2, 2) + "斤");
// 無參構造函數的方法引用,類似於靜態方法引用,只需要分析好輸入輸出即可
Supplier supplier = Dog::new;
System.out.println("創建了新對象:" + supplier.get());
// 有參構造函數的方法引用
Function function2 = Dog::new;
System.out.println("創建了新對象:" + function2.apply("旺財"));
}
}

最後需要說一句的就是能夠使用方法引用的地方就儘量不要使用lambda表達式,這樣就不會多生成一個類似 lambda$0 這樣的函數,能夠減少一些資源的開銷。


類型推斷

通過以上的例子,我們知道之所以能夠使用Lambda表達式的依據是必須有相應的函數接口。這一點跟Java是強類型語言吻合,也就是說你並不能在代碼的任何地方任性的寫Lambda表達式。實際上Lambda的類型就是對應函數接口的類型。Lambda表達式另一個依據是類型推斷機制,在上下文信息足夠的情況下,編譯器可以推斷出參數表的類型,而不需要顯式指名。

所以說 Lambda 表達式的類型是從 Lambda 的上下文推斷出來的,上下文中 Lambda 表達式需要的類型稱為目標類型,如下圖所示:

Java函數式編程和lambda表達式(轉自51 ZeroOne01)

接下來我們使用一個簡單的例子,演示一下 Lambda 表達式的幾種類型推斷,首先定義一個簡單的函數接口:

@FunctionalInterface
interface IMath {
int add(int x, int y);
}

示例代碼如下:

public class TypeDemo {
public static void main(String[] args) {
// 1.通過變量類型定義
IMath iMath = (x, y) -> x + y;
// 2.數組構建的方式
IMath[] iMaths = {(x, y) -> x + y};
// 3.強轉類型的方式
Object object = (IMath) (x, y) -> x + y;
// 4.通過方法返回值確定類型
IMath result = createIMathObj();
// 5.通過方法參數確定類型
test((x, y) -> x + y);
}
public static IMath createIMathObj() {
return (x, y) -> x + y;
}
public static void test(IMath iMath){
return;
}
}

變量引用

Lambda表達式類似於實現了指定接口的內部類或者說匿名類,所以在Lambda表達式中引用變量和我們在匿名類中引用變量的規則是一樣的。如下示例:

public static void main(String[] args) {
String str = "當前的系統時間戳是: ";
Consumer consumer = s -> System.out.println(str + s);
consumer.accept(System.currentTimeMillis());
}

值得一提的是,在JDK1.8之前我們一般會將匿名類裡訪問的外部變量設置為final,而在JDK1.8裡默認會將這個匿名類裡訪問的外部變量給設置為final。例如我現在改變str變量的值,ide就會提示錯誤:

Java函數式編程和lambda表達式(轉自51 ZeroOne01)

至於為什麼要將變量設置final,這是因為在Java裡沒有引用傳遞,變量都是值傳遞的。不將變量設置為final的話,如果外部變量的引用被改變了,那麼最終得出來的結果就會是錯誤的。

下面用一組圖片簡單演示一下值傳遞與引用傳遞的區別。以列表為例,當只是值傳遞時,匿名類裡對外部變量的引用是一個值對象:

Java函數式編程和lambda表達式(轉自51 ZeroOne01)

若此時list變量指向了另一個對象,那麼匿名類裡引用的還是之前那個值對象,所以我們才需要將其設置為final防止外部變量引用改變:

Java函數式編程和lambda表達式(轉自51 ZeroOne01)

而如果是引用傳遞的話,匿名類裡對外部變量的引用就不是值對象了,而是指針指向這個外部變量:

Java函數式編程和lambda表達式(轉自51 ZeroOne01)

所以就算list變量指向了另一個對象,匿名類裡的引用也會隨著外部變量的引用改變而改變:

Java函數式編程和lambda表達式(轉自51 ZeroOne01)


級聯表達式和柯里化

在函數式編程中,函數既可以接收也可以返回其他函數。函數不再像傳統的面向對象編程中一樣,只是一個對象的工廠或生成器,它也能夠創建和返回另一個函數。返回函數的函數可以變成級聯 lambda 表達式,特別值得注意的是代碼非常簡短。儘管此語法初看起來可能非常陌生,但它有自己的用途。

級聯表達式就是多個lambda表達式的組合,這裡涉及到一個高階函數的概念,所謂高階函數就是一個可以返回函數的函數,如下示例:

// 實現了 x + y 的級聯表達式
Function> function1 = x -> y -> x + y;
System.out.println("計算結果為: " + function1.apply(2).apply(3)); // 計算結果為: 5

這裡的 y -> x + y 是作為一個函數返回給上一級表達式,所以第一級表達式的輸出是 y -> x + y這個函數,如果使用括號括起來可能會好理解一些:

x -> (y -> x + y)

級聯表達式可以實現函數柯里化,簡單來說柯里化就是把本來多個參數的函數轉換為只有一個參數的函數,如下示例:

Function>> function2 = x -> y -> z -> x + y + z;
System.out.println("計算結果為: " + function2.apply(1).apply(2).apply(3)); // 計算結果為: 6

函數柯里化的目的是將函數標準化,函數可靈活組合,方便統一處理等,例如我可以在循環裡只需要調用同一個方法,而不需要調用另外的方法就能實現一個數組內元素的求和計算,代碼如下:

public static void main(String[] args) {
Function>> f3 = x -> y -> z -> x + y + z;
int[] nums = {1, 2, 3};
for (int num : nums) {
if (f3 instanceof Function) {
Object obj = f3.apply(num);
if (obj instanceof Function) {
f3 = (Function) obj;
} else {
System.out.println("調用結束, 結果為: " + obj); // 調用結束, 結果為: 6
}
}
}
}

級聯表達式和柯里化一般在實際開發中並不是很常見,所以對其概念稍有理解即可,這裡只是簡單帶過,若對其感興趣的可以查閱相關資料。


分享到:


相關文章: