什麼是函數語言程式設計
通過整合現有程式碼來產生新的功能,而不是從零開始編寫所有內容,由此我們會得到更加可靠的程式碼,並獲得更高的效率
我們可以這樣理解:物件導向程式設計抽象資料,函數語言程式設計抽象行為。
通常而言,方法會根據所傳遞的資料產生不同的結果,但如果想讓一個方法在每次呼叫時都有不同的表現該怎麼辦?你可能會想到多型,沒錯,多型是一種通過改變實際執行的方法所屬物件,以此來改變實際執行的方法程式碼的方式,許多框架實現IoC本質上也是實現了一種自動化的多型。而這裡我們要聊聊其他的兩種方法。
如果我們直接將程式碼傳遞給方法,就可以控制其行為,Java 8以後,我們可以通過lambda表示式和方法參照這兩個新的方法來實現這一點。
什麼是函數式介面
什麼是函數式介面?這是我們理解Lambda表示式以及方法參照的重點,這些介面是lambda表示式和方法參照的目標型別,這裡我們參照一個比較容易理解的說法:函數式介面是一個只有一個抽象方法 (不包含祖先類Object中的公共方法,如hashcode()
等) 的介面。
當我們在編寫介面時,這種函數式方法模式可以使用@FunctionalInterface
註解來強制實施,如果被註解的介面不符合標準那麼就會在編譯時報錯。下面給出例子:
interface Functional{// 是函數式介面
String speak();
}
interface NoFunctional{// 不是函數式介面
String speak();
String laugh();
}
interface IsFunctional{// 是函數式介面,因為toString()是Object祖先類的公共方法,不算在內
String spaek();
String toString();
}
它們的意義是什麼呢?
這裡拿出一個例子:
interface Say{
void say();
}
class Speaker{
public static void speak(){
System.out.println("Hello, my friend!");
}
public static void main(String[] args){
Say say = Speaker::speak; // 這裡的::表示我們參照了Speaker類的speak方法
say.say();
// 輸出Hello, my friend!
}
}
很神奇是不是?我們直接將一個方法參照賦給了一個介面的物件。這裡初看的話顯然問題一堆,首先方法怎麼能作為物件賦值,其次該類也沒有實現該介面,最後,就算能說通,我們的介面和Speaker
類根本沒有相同的方法啊!怎麼就呼叫say.say()
效果等同於Speaker.speak()
呢?
重要:這是Java 8增加的一個小魔法:如果我們將一個方法參照或一個lambda表示式賦給一個函數式介面(且兩個方法的返回值型別和引數型別可以匹配上,方法名並不重要),那麼Java會自動調整這個賦值操作,使其能夠匹配目標介面。
對於底層來說,這裡是Java編譯器建立一個實現了目標介面的類的範例,並將我們的方法參照或lambda表示式包裹在其中。
事實上,很容易預見,在這裡如果我們直接將一個Speaker
物件賦給一個Say
介面,那麼是無法做到的,因為Speaker
雖然符合Say
的模式,但卻沒有顯式的實現Say
介面。幸運的是,Java 8的函數式介面允許我們直接把一個實體方法賦給這樣的一個介面,這樣語法更好,更簡單。
Java為我們準備了大量的函數式介面,這樣我們就可以儘量避免自己建立大量的介面,這些介面都可以在Java.util.function
包中輕鬆找到。
不過Java最多隻準備了具有兩個引數的函數式介面,但是介面又不難寫,只要我們理解了函數式介面的意義和用法,自己寫一個能容納更多引數的函數式介面不過是信手拈來罷了。
// 舉個例子,一個有四個泛型引數的函數式介面
@FunctionalInterface
public interface TriFunction<T, U, V, R>{
R apply(T t, U u, V v, R r);
}
補充
在Java給出的函數式介面中,我們只能看到一部分涉及基本型別的函數式介面。其餘都是由泛型完成的,為什麼要這麼做呢?估計就是因為對於某些很常用的函數式介面,如整形輸入+Double輸出這樣。如果我們採用泛型Function<Integer, Double>
來實現,就涉及到自動裝箱和自動拆箱的效能問題,如果該方法會被大量呼叫,那麼還是直接宣告清楚其中的型別對於我們的整體效能更為有利
lambda表示式本質上是一個匿名方法,其中以->
為分隔符,在其前的是輸入引數,在其後的是返回變數。
lambda表示式是函數式介面的其中一個"成果"——另一個是方法參照,我們可以將相同輸入輸出引數 (型別和數量都匹配) 的lambda表示式賦給一個函數式介面,通過呼叫賦值後的介面再來呼叫我們建立的lambda表示式。
lambda表示式的語法如下所示:
->
,它可以讀作產生(produces)msg -> msg + "!";
msg -> msg.upperCase();
() -> "hello world";
h -> {
System.out.println(h + "abcdefg");
return h.lowerCase();
}
有以下這些問題需要注意:
return
關鍵字是不合法的return
返回結果與內部匿名類相比,lambda表示式的可讀性極佳,所以如果你需要使用這樣的方法,你應該掌握lambda表示式。
注意,如果你要利用lambda表示式實現遞迴呼叫自身的話,必須要將該表示式賦值給一個靜態變數或一個範例變數,否則該語句對於編譯器可能過於複雜,會產生編譯錯誤。
方法參照指向的是方法,通過類名或物件名,跟::
,然後跟方法名就可以實現方法參照,注意,這種方法並不需要在方法名後面加上參數列。
className::method; // 注意,沒有括號!
我們可以通過一個介面,參照那些簽名 (方法的引數型別數量以及返回型別) 一致的方法。
需要注意的是,任何方法要被參照,都需要該方法存在其繫結物件,這個物件對於靜態方法而言就是其Class物件,而對於普通方法而言,則需要確儲存在該類物件。
該物件要麼被你建立出來並顯式的賦給方法參照,類似這樣
Object o = new o;
method(o::methodName);
這樣我們的編譯器就知道,這個方法是由我們的物件o
來執行的,非常的明確。
但是,還有一種情況就是未繫結的方法參照,即尚未關聯到某個物件的普通方法。對於這種參照,必須先提供物件,才能進行使用。
interface Showable{
String show(Show1 s);
}
class Show1{
public String show(){
return "hello world";
}
}
public class test {
public static void main(String[] args){
Show1 s = new Show1();
Showable sa = Show1::show;
System.out.println(s.show());
// 關鍵就在這裡,我們知道這裡sa實際上沒有範例,show()當然無法呼叫,但實際上這裡最終會呼叫到物件s的show()方法
// 實際上,這裡Java知道,它必須接受這個引數s,並且在s上面呼叫方法
// 如果方法有更多引數,只需遵循第一個引數取得是this這種模式
System.out.println(sa.show(s));
}
}
若不只一個引數,則如下
public class test {
public static void main(String[] args){
Show1 s = new Show1();
Showable sa = Show1::show;
System.out.println(s.show(1, 2));
System.out.println(sa.show(s, 1, 2));
}
}
interface Showable{
String show(Show1 s, int a, int b);
}
class Show1{
public String show(int a, int b){
return "hello world";
}
}
實際上,除了普通的方法外,我們還可以通過new
代替方法名來參照其構造器方法,這也是非常有用的,由於構造器方法實際上繫結該類,相當於靜態方法,所以我們只需要通過介面直接呼叫即可。