高效開發(fā)!Lambda表達(dá)式和函數(shù)式接口最佳實(shí)踐
環(huán)境:Spring Boot 3.2.5
1. 簡介
Lambda表達(dá)式與函數(shù)式接口是Java 8引入的重要特性,它們極大地簡化了代碼編寫,提升了代碼的可讀性和簡潔性。Lambda表達(dá)式提供了一種簡潔的方式來表示匿名函數(shù),而函數(shù)式接口則是一種只包含一個抽象方法的接口,非常適合與Lambda表達(dá)式結(jié)合使用。
在實(shí)際開發(fā)中,合理運(yùn)用這些特性可以提高代碼的靈活性和復(fù)用性。最佳實(shí)踐中,推薦首先定義清晰且具有描述性的函數(shù)式接口,以增強(qiáng)代碼的可理解性;其次,在使用Lambda表達(dá)式時,盡量保持其簡短且功能單一,避免復(fù)雜的邏輯嵌套;此外,利用Stream API等函數(shù)式編程工具進(jìn)行集合操作,能夠使代碼更加流暢、高效。通過遵循這些原則,開發(fā)者不僅能夠?qū)懗龈觾?yōu)雅的代碼,還能更好地應(yīng)對并發(fā)編程的需求,提升程序的整體性能。
接下來,我們將詳細(xì)研究函 數(shù)式接口 和 lambda 表達(dá)式。
2. 最佳實(shí)踐
2.1 優(yōu)先使用標(biāo)準(zhǔn)的函數(shù)式接口
在 java.util.function 包中定義的函數(shù)式接口滿足了大多數(shù)開發(fā)者為 lambda 表達(dá)式和方法引用提供目標(biāo)類型的需求。這些接口中的每一個都是通用且抽象的,這使得它們能夠輕松適應(yīng)幾乎任何 lambda 表達(dá)式。我們在創(chuàng)建自定義的函數(shù)式接口之前,應(yīng)該優(yōu)先查看該包中的定義。
我們先來看下如下接口:
@FunctionalInterface
public interface Foo {
String xxxooo(String string) ;
}
實(shí)用該接口:
public class UseFoo {
public String pack(String param, Foo foo) {
return foo.xxxooo(param) ;
}
}
運(yùn)行程序應(yīng)該是如下方式:
Foo foo = param -> String.format("%s other info", param) ;
String ret = new UseFoo().pack("Message ", foo) ;
在這里的Foo接口方法簽名需要一個入?yún)⑷缓蠓祷匾粋€參數(shù)。而Java 8 已經(jīng)在 java.util.function 包中提供了這樣的接口 Function<T, R>。所以我們沒有必要自己在定義,可以將上面的UseFoo修改如下:
public String pack(String param, Function<String, String> foo) {
return foo.apply(param) ;
}
// 調(diào)用
Function<String, String> foo = param -> String.format("%s other info", param) ;
使用與之前定義的基本相同。
2.2 使用@FunctionalInterface注解
使用 @FunctionalInterface 注解接口。乍一看,這個注解似乎沒有什么用處。即使沒有它,只要接口中只有一個抽象方法,該接口也會被視為函數(shù)式接口。
然而,如果在一個大型項(xiàng)目中有多個接口;手動控制所有接口是很困難的。一個原本設(shè)計(jì)為函數(shù)式的接口可能會因?yàn)椴恍⌒奶砑恿肆硪粋€抽象方法而被改變,從而使它不再是一個有效的函數(shù)式接口。
通過使用 @FunctionalInterface 注解,編譯器會在任何試圖破壞函數(shù)式接口預(yù)定義結(jié)構(gòu)的行為時觸發(fā)錯誤。如下示例:
@FunctionalInterface
public interface Foo {
String xxxooo(String param) ;
}
使用該接口,能防止你再定義其它方法。
2.3 不要在函數(shù)式接口中過渡使用默認(rèn)方法
我們可以輕松地在函數(shù)式接口中添加默認(rèn)方法。只要接口中只有一個抽象方法聲明,這樣做是符合函數(shù)式接口契約的:
@FunctionalInterface
public interface Foo {
String xxxooo(String param);
default void defaultMethod() {
// ...
}
}
如果它們的抽象方法具有相同的簽名,函數(shù)式接口可以被其他函數(shù)式接口繼承:
@FunctionalInterface
public interface Zoo extends Baz, Bar {}
@FunctionalInterface
public interface Baz {
String xxxooo(String param);
default String defaultBaz() {
return "Baz..." ;
}
}
@FunctionalInterface
public interface Bar {
String xxxooo(String param);
default String defaultBar() {
return "Bar..." ;
}
}
public static void main(String[] args) {
Zoo zoo = param -> String.format("%s extends", param) ;
System.out.println(zoo.xxxooo("Functional Interface")) ;
}
就像普通的接口一樣,如果不同的函數(shù)式接口繼承了具有相同默認(rèn)方法的接口,這也可能會帶來問題。
修改上面的Baz和Bar接口,添加相同的默認(rèn)方法:
@FunctionalInterface
public interface Baz {
default String print(){
// ...
}
}
@FunctionalInterface
public interface Bar {
default String print(){
// ...
}
}
這樣定義后,Zoo接口將編譯不通過,重復(fù)的默認(rèn)方法錯誤。
我們可以通過如下方式,在Zoo接口重寫defaultCommon方法,如下示例:
@FunctionalInterface
public interface Zoo extends Baz, Bar {
@Override
default String print() {
return Bar.super.print() ;
}
}
所以,我們不應(yīng)該在函數(shù)式接口中定義過多的默認(rèn)方法。
2.4 使用 Lambda 表達(dá)式實(shí)例化功能接口
編譯器允許我們使用內(nèi)部類來實(shí)例化函數(shù)式接口;然而,這樣做會導(dǎo)致代碼非常冗長。我們應(yīng)該優(yōu)先使用 lambda 表達(dá)式:
// 是使用上面定義的Zoo接口
Zoo zoo = param -> String.format("%s extends", param) ;
System.out.println(zoo.xxxooo("Functional Interface")) ;
如果是內(nèi)部類定義那就太不優(yōu)雅了。
Zoo zoo = new Zoo() {
public String xxxooo(String param) {
return String.format("%s extends", param) ;
}
} ;
現(xiàn)在開發(fā)工具都能自動幫你將這里的內(nèi)部類轉(zhuǎn)換為lambda表達(dá)式。
2.5 避免重載帶有函數(shù)式接口作為參數(shù)的方法
public interface Processor {
String process(Callable<String> c) throws Exception;
String process(Supplier<String> s);
}
public class ProcessorImpl implements Processor {
public String process(Callable<String> c) throws Exception {
return c.call() ;
}
public String process(Supplier<String> s) {
return s.get() ;
}
}
上面代碼看著沒撒毛病,但是你通過lambda表達(dá)傳參時,就出問題了:
ProcessorImpl process = new ProcessorImpl() ;
process.process(() -> "Pack") ;
Eclipse下提示
圖片
模棱兩可的方法調(diào)用。解決辦法有2種:
- 定義不同的方法名稱
- 強(qiáng)制轉(zhuǎn)換
ProcessorImpl process = new ProcessorImpl() ;
process.process((Supplier<String>)() -> "Pack") ;
但是不推薦這種方式。
2.6 不要將 Lambda 表達(dá)式視為內(nèi)部類
盡管在前面的例子中,我們基本上是用 Lambda 表達(dá)式替換了內(nèi)部類,但這兩個概念在一個重要方面是不同的:作用域。
當(dāng)我們使用內(nèi)部類時,它會創(chuàng)建一個新的作用域。我們可以通過實(shí)例化具有相同名稱的新局部變量來隱藏外部作用域中的局部變量。我們還可以在內(nèi)部類中使用 this 關(guān)鍵字作為對其自身實(shí)例的引用。
然而,Lambda 表達(dá)式則與外部作用域一起工作。我們不能在 Lambda 表達(dá)式的主體中隱藏外部作用域中的變量。在這種情況下,this 關(guān)鍵字是對外部實(shí)例的引用。
private String value = "Outer class value";
@FunctionalInterface
public interface Foo {
String fn(String param);
}
public void xxoo() {
Foo f = new Foo() {
String value = "Inner class value";
@Override
public String fn(String param) {
return this.value;
}
};
String ret = f.fn("Pack") ;
System.out.println(ret) ;
Foo fl = param -> {
String value = "Lambda value";
return this.value;
};
ret = fl.fn("Pack");
System.out.println(ret) ;
}
輸出結(jié)果:
Inner class value
Outer class value
根據(jù)運(yùn)行結(jié)果得知,在Lambda中this.value方法的是類中定義的變量,而內(nèi)部類訪問的則是當(dāng)前內(nèi)部類的變量。
2.7 避免在 Lambda 表達(dá)式的主體中使用代碼塊
Lambda 表達(dá)式應(yīng)該用一行代碼來編寫。通過這種方式,Lambda 表達(dá)式成為一個自解釋的結(jié)構(gòu),聲明了應(yīng)該對哪些數(shù)據(jù)執(zhí)行什么操作。
如果我們有一大段代碼,那么 lambda 的功能就不會立即顯現(xiàn)出來。
Foo foo = param -> buildString(param) ;
private String buildString(String param) {
String result = "Something " + param ;
// ...
return result ;
}
而不應(yīng)該是如下代碼
Foo foo = param -> {
String result = "Something " + param ;
// ...
return result ;
} ;
注意:如果 lambda的定義有兩三行代碼,那么將代碼提取到另一個方法中也沒有什么價值,我們不應(yīng)該將 "單行 lambda" 完全作為一個規(guī)約。
2.8 避免指定參數(shù)類型
在大多數(shù)情況下,編譯器可以通過類型推斷來確定 lambda 參數(shù)的類型。 因此,為參數(shù)添加類型是可選的,可以省略,如下實(shí)例:
BiFunction<String, String, String> fun =
(String a, String b) -> a.toLowerCase() + b.toLowerCase() ;
這里我們不用聲明類型,而是如下方式:
BiFunction<String, String, String> fun =
(a, b) -> a.toLowerCase() + b.toLowerCase() ;
這里完全可以通過類型推斷確定類型,所以沒有必要什么參數(shù)的類型。
2.9 單參數(shù)不要使用括號
Lambda 語法只要求在多個參數(shù)或沒有參數(shù)時使用括號。
錯誤示例
Function<String, String> fun = (a) -> a.toLowerCase() ;
正確示例
Function<String, String> fun = a -> a.toLowerCase() ;
只有一個參數(shù)時沒有必要添加括號
2.10 避免返回語句和括號
理想情況下,Lambda 表達(dá)式應(yīng)該用一行代碼來編寫。通過這種方式,Lambda 表達(dá)式成為一個自解釋的結(jié)構(gòu),聲明了應(yīng)該對哪些數(shù)據(jù)執(zhí)行什么操作。
錯誤示例
Function<String, String> func = a -> {return a.toLowerCase()};
正確示例
Function<String, String> func = a -> a.toLowerCase() ;
這里我們沒有必要使用代碼塊,我們應(yīng)該時刻注意盡可能的使得 Lambda 表達(dá)式只有一行。
2.11 方法引用
很多時候,即使在我們之前的示例中,lambda 表達(dá)式也只是調(diào)用其他地方已經(jīng)實(shí)現(xiàn)的方法。 在這種情況下,使用 Java 8 的另一個特性--方法引用就非常有用了。
錯誤示例
Function<String, String> func = a -> a.toLowerCase();
正確示例
Function<String, String> func = String::toLowerCase;
如果你不懂方法引用,那么這種寫法是不是可讀性不好了?
2.12 使用"Effectively Final"變量
在 Lambda 表達(dá)式內(nèi)部訪問 非final 變量會導(dǎo)致編譯時錯誤,但這并不意味著我們應(yīng)該將每個目標(biāo)變量都標(biāo)記為 final。根據(jù) "Effectively final" 的概念,只要變量僅被賦值一次,編譯器就會將其視為 final。
public void xxxooo() {
String value = "Local" ; // 這里我們可以省去 final 修飾符
Function<String, String> func = str -> {
return value ;
} ;
}
這里我們沒有必要在變量value前使用 final 修飾。但是我們不能在代碼塊中去修改,如下將無法編譯通過: