承接上文:Java函數語言程式設計:一、函數式介面,lambda表示式和方法參照
這次來聊聊函數語言程式設計中其他的幾個比較重要的概念和技術,從而使得我們能更深刻的掌握Java中的函數語言程式設計。
本篇部落格主要聊聊以下幾個問題
高階函數這裡指的可不是數學裡的那個,這裡主要要從維度這個概念理解,本來函數生成的是值,也就是說,函數比值高維,那麼如果我們有一個函數能生成函數或者是以函數為引數,那麼顯然就比普通的生成值的函數更高維,因為我可以生成你。
定義:高階函數是一個能接受函數作為引數或能夠把函數作為返回值的函數。
public interface Function{
String str(String s);
}
public class Procudure{
// 下面就是一個標準的高階函數
public Function(String s){
return s -> s.upperCase();
}
}
這裡有兩點:
java.util.function
中的介面,或是自定義一個函數式介面來為專門的介面建立別名但是這只是基本的,還記得函數語言程式設計的意義嗎?這裡的關鍵在於,有時候,我們可以根據接受的函數,讓高階函數生成一個新的函數。
public class test {
public static Function<String, String> transform(Function<String, String> f){
return f.andThen(String::toUpperCase);
}
public static void main(String[] args) {
Function<String, String> transform = test2.transform(str -> {
return str.substring(0, 2);
});
String s = transform.apply("abcdefg");
System.out.println(s);
}
}
可以看到,這裡我們通過andThen()
這個Function
介面的通用方法,連線了前後兩個方法,並且使得無論我們輸入了什麼,都會將該字串轉化為全大寫,後面我們輸入了一個擷取前兩個字元作為返回值的方法,但很明顯,這裡可以有更多的選擇,並且我們實際上也可以通過方法參照來參照某些定義好的函數,非常靈活。
什麼是閉包?
考慮一個lambda表示式,它使用了其函數作用域之外的變數。當返回該函數時會發生什麼呢?也即,當我們通過呼叫lambda表示式產生的匿名方法參照這些外部變數會發生什麼呢?
如果一門語言能夠解決這個問題,我們就認為該語言是支援閉包的,或者也可以說它支援詞法作用域。
這裡還涉及到一個術語:變數捕獲
上面聽起來是不是不明白,沒關係,給個例子:
public class Example{
IntSupplier plus(int x){
int y = 1;
return () -> x + y;
}
}
考慮這個類和其中的方法plus(int x)
,你會不會發現有一些問題。
因為我們的plus(int x)
方法返回的是一個函數,這裡假設返回的函數是f(int x)
,也就是說,f(int x)
返回時,plus(int x)
已經執行結束,所以其中的變數int y = 1;
已經脫離了作用域,那麼等到我們獲取了f(int x)
的物件再呼叫到f(int x)
方法時,這個y
要怎麼辦呢?
你會發現,上面的這個方法是可以被編譯執行成功的,但是下面的這個就不行:
public class Example{
IntSupplier plus(int x){
int y = 1;
return () -> x + (++y);
}
}
為什麼呢?編譯器提示:lambda 表示式中使用的變數應為 final 或有效 final
這句話就說的很明白了,對於第一個例子,我們的y
雖然沒final
關鍵字,但它是事實上的final
變數,一旦這裡賦值就不會再改動,而對於第二個方法來說則相當於把y
賦予了新的值。
這裡如果我們使用的是參照,比如下面這個例子
public class Example{
IntSupplier plus(int x){
Queue<Integer> y = new LinkedList<>();
y.offer(1);
return () -> x + y.poll();
}
}
注意,這裡是可以通過編譯的,因為實際上我們只需要保證這個參照所指向的物件不被修改,避免後面呼叫返回的函數時卻突然發現找不到對應的物件即可。
所以,Java提供的閉包的條件是,我們必須要能夠保證,被捕獲的變數是final
的。
不過要注意的是,這裡如果是多執行緒情況的話,不能保證執行緒安全。
之前我們有提到andThen()
這個方法,這些方法在Java.util.function
包中的各個函數式介面中各有提供,總的來說有這麼幾種:
andThen(arg)
compose(arg)
and(arg)
or(arg)
negate()
在後面的流處理章節,你將會體會到這些函陣列合的力量。
所謂柯里化,就是指將一個接受多個引數的函數轉變為一系列,只接受一個引數的函數,在面向函數程式設計裡這麼做的目的,就跟我們在物件導向程式設計裡需要抽象出介面和抽象類是一樣的,目的就是我們可以通過部分求值來複用這些程式碼。
public class Currying{
// 未柯里化的函數
static String unCurried(String a,String b){
return a + b;
}
// 柯里化的函數
static Function<String, Function<String, String>> Curried(){
return a -> b -> a + b;
}
// 範例
public static void main(String[] args) {
Function<String, Function<String, String>> curried = test2.Curried();
System.out.println(unCurried("hello ", "World"));
Function<String, String> firstWord = curried.apply("hello ");
System.out.println(firstWord.apply("World"));
System.out.println(firstWord.apply("My friend"));
System.out.println(firstWord.apply("My love"));
}
/**
輸出
hello World
hello World
hello My friend
hello My love
**/
}
簡單來說就是每一層都返回下一層的函數,直到最終返回我們需要的值為止。