Java函數式編程:一、函數式介面,lambda表達式和方法引用
- 2022 年 10 月 21 日
- 筆記
- 讀書筆記-Java基礎
Java函數式編程
什麼是函數式編程
通過整合現有程式碼來產生新的功能,而不是從零開始編寫所有內容,由此我們會得到更加可靠的程式碼,並獲得更高的效率
我們可以這樣理解:面向對象編程抽象數據,函數式編程抽象行為。
通常而言,方法會根據所傳遞的數據產生不同的結果,但如果想讓一個方法在每次調用時都有不同的表現該怎麼辦?你可能會想到多態,沒錯,多態是一種通過改變實際執行的方法所屬對象,以此來改變實際執行的方法程式碼的方式,許多框架實現IoC本質上也是實現了一種自動化的多態。而這裡我們要聊聊其他的兩種方法。
如果我們直接將程式碼傳遞給方法,就可以控制其行為,Java 8以後,我們可以通過lambda表達式和方法引用這兩個新的方法來實現這一點。
1、函數式介面
什麼是函數式介面
什麼是函數式介面?這是我們理解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>來實現,就涉及到自動裝箱和自動拆箱的性能問題,如果該方法會被大量調用,那麼還是直接聲明清楚其中的類型對於我們的整體性能更為有利
2、Lambda表達式
lambda表達式本質上是一個匿名方法,其中以->為分隔符,在其前的是輸入參數,在其後的是返回變數。
lambda表達式是函數式介面的其中一個"成果"——另一個是方法引用,我們可以將相同輸入輸出參數 (類型和數量都匹配) 的lambda表達式賦給一個函數式介面,通過調用賦值後的介面再來調用我們創建的lambda表達式。
lambda表達式的語法如下所示:
- 參數
->,它可以讀作產生(produces)- 方法體
msg -> msg + "!";
msg -> msg.upperCase();
() -> "hello world";
h -> {
System.out.println(h + "abcdefg");
return h.lowerCase();
}
有以下這些問題需要注意:
- lambda表達式如果只有一個參數,可以只寫該參數不寫括弧,但你要知道這是一種特殊情況而不是相反
- 通常需要括弧將參數包裹起來,為了一致性考慮,單個參數時也可以使用括弧
- 沒有參數時,必須要有括弧指代輸入的參數
- 存在多個參數時必須要以括弧包裹,逗號分割
- 如果一句話就可以表示返回值,那麼就直接寫到方法體所在的位置,此時
return關鍵字是不合法的 - 如果一句話無法囊括,那麼就需要使用花括弧將所有函數體包裹並以
return返回結果
與內部匿名類相比,lambda表達式的可讀性極佳,所以如果你需要使用這樣的方法,你應該掌握lambda表達式。
注意,如果你要利用lambda表達式實現遞歸調用自身的話,必須要將該表達式賦值給一個靜態變數或一個實例變數,否則該語句對於編譯器可能過於複雜,會產生編譯錯誤。
3、方法引用
方法引用指向的是方法,通過類名或對象名,跟::,然後跟方法名就可以實現方法引用,注意,這種方法並不需要在方法名後面加上參數列表。
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代替方法名來引用其構造器方法,這也是非常有用的,由於構造器方法實際上綁定該類,相當於靜態方法,所以我們只需要通過介面直接調用即可。

