1.概覽
隨著Java 8的廣泛使用,開始有人為其新增特性總結(jié)最佳實踐,在本教程中,我們來討論一下函數(shù)式接口與Lambda表達式。
2.使用標(biāo)準的函數(shù)式接口
java.util.function包的函數(shù)式接口,滿足了大部分程序員在使用Lambda表達式和方法引用時,對目標(biāo)類型的需求。這些抽象的接口可以輕松適配大部分Lambda表達式。在創(chuàng)建新的函數(shù)表達式前,開發(fā)者應(yīng)該好好研究一下這個包。
假設(shè)有一個叫Foo的接口:
@FunctionalInterface
public interface Foo {
String method(String string);
}
和一個UseFoo類,里面有add()的方法,它使用Foo接口作為參數(shù)。
public String add(String string, Foo foo) {
return foo.method(string);
}
你可能會這樣執(zhí)行方法:
Foo foo = parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", foo);
仔細檢查代碼,你會發(fā)現(xiàn)Foo僅僅是接受一個參數(shù)并返回結(jié)果的函數(shù)。Java已在java.util.function包中的[Function<T,R>]提供同樣接口。
現(xiàn)在我們可以完全刪除Foo,并把代碼改為:
public String add(String string, Function<String, String> fn) {
return fn.apply(string);
}
然后這樣執(zhí)行方法:
Function<String, String> fn =
parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", fn);
3.使用@FunctionalInterface注解你的函數(shù)式接口。在開始時,該注解似乎并無意義——哪怕不加注解,只要接口有且僅有一個抽象方法,它就會被看成函數(shù)式接口。
但是假設(shè)現(xiàn)在有個大項目,其中包含多個接口,這時就很難把控全局。一個本被設(shè)計為函數(shù)式接口的接口,或會因被意外加上其他抽象方法,而失去了函數(shù)式接口的功能。
而使用@FunctionalInterface注解后,每當(dāng)編譯器發(fā)現(xiàn)任何試圖破壞函數(shù)式接口結(jié)構(gòu)的改動,就會報錯。這樣一來,其他開發(fā)者就能輕松理解該項目的結(jié)構(gòu)。
所以,請這樣寫:
@FunctionalInterface
public interface Foo {
String method();
}
而非這樣:
public interface Foo {
String method();
}
4.不要濫用函數(shù)式接口的默認方法
你可以輕而易舉地在函數(shù)式接口中添加默認方法,只要遵守“接口只含一個抽象方法”的規(guī)定,就不會有問題:
@FunctionalInterface
public interface Foo {
String method();
default void defaultMethod() {}
}
如果抽象方法的方法簽名一樣,函數(shù)式接口就可以被其他函數(shù)式接口繼承。例如:
@FunctionalInterface
public interface FooExtended extends Baz, Bar {}
@FunctionalInterface
public interface Baz {
String method();
default void defaultBaz() {}
}
@FunctionalInterface
public interface Bar {
String method();
default void defaultBar() {}
}
與普通接口一樣,使用同一默認方法繼承不同的函數(shù)式接口會產(chǎn)生許多問題。例如,假設(shè)Bar 和 Baz各有一個叫defaultCommon()的默認方法,這樣就會發(fā)生編譯時錯誤:
interface Foo inherits unrelated defaults for defaultCommon() from types Baz and Bar...
你需要在Foo 接口中,覆蓋defaultCommon() 方法才能修復(fù)該問題。當(dāng)然,你也可以為該方法提供自定義實現(xiàn)。但如果你想使用其中一個父類接口的實現(xiàn)(例如,Baz接口),就需要在defaultCommon()方法體中添加如下代碼:
Baz.super.defaultCommon();
但要小心,在接口中增加太多默認方法,會帶來架構(gòu)上的混亂。你應(yīng)把默認方法看成在既要更新已有的接口,又要保持原有兼容性時,一種無可奈何的折衷。
5.使用Lambda表達式實例化函數(shù)式接口
編譯器允許你使用內(nèi)部類實例化函數(shù)式接口,不過這樣會導(dǎo)致代碼繁瑣,使用Lambda是更好的選擇:
Foo foo = parameter -> parameter + " from Foo";
而不是這樣:
Foo fooByIC = new Foo() {
@Override
public String method(String string) {
return string + " from Foo";
}
};
Lambda表達式對很多舊的庫都有效。例如是Runnable,Comparator之類。但這不等于需要你把舊的代碼全部改為Lambda。
6.避免重載參數(shù)帶有函數(shù)式接口的方法
使用不同的方法名去避免沖突;來看看一個例子:
public interface Processor {
String process(Callable<String> c) throws Exception;
String process(Supplier<String> s);
}
public class ProcessorImpl implements Processor {
@Override
public String process(Callable<String> c) throws Exception {
// implementation details
}
@Override
public String process(Supplier<String> s) {
// implementation details
}
}
初看之下貌似并無異樣,但只要試圖執(zhí)行ProcessorImpl下面的其中一個方法:
String result = processor.process(() -> "abc");
就會出現(xiàn)如下錯誤信息:
reference to process is ambiguous
both method process(java.util.concurrent.Callable<java.lang.String>)
in com.baeldung.java8.lambda.tips.ProcessorImpl
and method process(java.util.function.Supplier<java.lang.String>)
in com.baeldung.java8.lambda.tips.ProcessorImpl match
我們可以用兩個方法解決這個問題。第一,使用不同的方法名:
String processWithCallable(Callable<String> c) throws Exception;
String processWithSupplier(Supplier<String> s);
第二是手工轉(zhuǎn)型,不推薦這樣做。
String result = processor.process((Supplier<String>) () -> "abc");
7.不要把Lambda看成是內(nèi)部類
之前的例子里,我們使用Lambda替代內(nèi)部類,但兩者有個很大的不同點:域。
在創(chuàng)建內(nèi)部類時,也創(chuàng)造了一個新的域。你可以在私有域中,新建名稱相同的本地變量。你還可以在內(nèi)部類使用this關(guān)鍵字代指該(內(nèi)部類的)實例。
例如,類UseFoo有一個實例變量:
private String value = "Enclosing scope value";
然后在這個類寫下如下代碼并執(zhí)行:
public String scopeExperiment() {
Foo fooIC = new Foo() {
String value = "Inner class value";
@Override
public String method(String string) {
return this.value;
}
};
String resultIC = fooIC.method("");
Foo fooLambda = parameter -> {
String value = "Lambda value";
return this.value;
};
String resultLambda = fooLambda.method("");
return "Results: resultIC = " + resultIC +
", resultLambda = " + resultLambda;
}
執(zhí)行scopeExperiment()方法會得到如下結(jié)果:
Results: resultIC = Inner class value, resultLambda = Enclosing scope value
如你所見,fooIC中的this.value返回其內(nèi)部類的本地變量。Lambda的this.value卻對Lambda方法體內(nèi)的值視若無睹,返回了UseFoo類的同名變量值。
8.讓Lambda保持簡潔易懂
如情況允許,盡可能用單行結(jié)構(gòu),而非一大塊代碼。要記住,Lambda是表達式,而非敘述體。雖然結(jié)構(gòu)簡單,但Lambda應(yīng)該清晰明了。
這僅僅是代碼風(fēng)格建議,雖然它并不會大幅提高性能,但這種風(fēng)格讓代碼更易閱讀,更親和。
8.1 避免在Lambda方法體內(nèi)使用代碼塊
理想情況下,Lambda應(yīng)該是一行而就。這種結(jié)構(gòu)讓它清晰易懂,別人能明白它使用什么數(shù)據(jù)(在Lambda有參數(shù)的情況下),干了什么事情。
如果你使用了代碼塊,Lambda的功能就變得不那么顯而易見。
帶著上面思路,看如下代碼:
Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
String result = "Something " + parameter;
//many lines of code
return result;
}
而不是:
Foo foo = parameter -> { String result = "Something " + parameter;
//many lines of code
return result;
};
但是,也無需把“Lambda只需一行”視為教條。如果只有兩三行代碼,或許沒必要把這些代碼抽出來化為方法。
8.2 避免指定參數(shù)類型
在大部分情況下,編譯器使用類型判斷功能足以得知Lambda的參數(shù)類型。因此,可忽略參數(shù)中類型。
應(yīng)該這樣:
(a, b) -> a.toLowerCase() + b.toLowerCase();
而不是這樣:
(String a, String b) -> a.toLowerCase() + b.toLowerCase();
8.3 單參數(shù)時,無需使用括號
根據(jù)Lambda語法,只有在多個參數(shù),或者完全沒有參數(shù)時,才需要使用括號。所以,如果只有一個參數(shù),可大膽的把括號去掉,簡化代碼。
應(yīng)該這樣:
a -> a.toLowerCase();
而不是這樣:
(a) -> a.toLowerCase();
8.4 避免使用大括號和Return
在Lambda的單行方法中,大括號和Return是可選項。為了簡潔,可忽略掉。
應(yīng)該這樣:
a -> a.toLowerCase();
而不是這樣:
a -> {return a.toLowerCase()};
8.5 使用方法引用
在之前的例子中,Lambda往往只是調(diào)用在別處已經(jīng)實現(xiàn)的方法。如此一來,我們便可以使用Java8的另一個特性:方法引用。
因此,這句Lambda:
a -> a.toLowerCase();
可替換成:
String::toLowerCase;
或許代碼短不了多少,但這樣更易懂。
9.使用“有效final”變量
在Lambda表達式中,訪問非final變量會導(dǎo)致編譯錯誤。但這不等于你要把所有變量都改為final。
根據(jù)“有效final”概念,只要某個變量只被賦值一次,它就會看成是final變量。
編譯器會控制Lambda內(nèi)的變量狀態(tài),但凡發(fā)現(xiàn)任何更改變量的意圖,就會拋出編譯錯誤,所以可大膽的在Lambda內(nèi)使用變量。
例如,以下的代碼無法通過編譯:
public void method() {
String localVariable = "Local";
Foo foo = parameter -> {
String localVariable = parameter;
return localVariable;
};
}
編譯器會告訴你:
Variable 'localVariable' is already defined in the scope.
這個功能會讓Lambda執(zhí)行時變得線程安全。
10.防止變量發(fā)生更變
Lambda的其中一個主要用途就是并發(fā)計算——這意味著它們在線程安全上能大派用場。
“有效final”特性雖能杜絕大部分問題,但凡事皆有例外。
Lambda方法體內(nèi)雖無法改變變量的值,但卻可改變可變對象的狀態(tài)。
思考如下代碼:
int[] total = new int[1];
Runnable r = () -> total[0]++;
r.run();
這段代碼是非法的,雖然total變量屬于“有效final”。但在執(zhí)行Lambda后,它指向的還是同一個引用狀態(tài)嗎?不!
以該段代碼為鑒,避免寫出會產(chǎn)生不可預(yù)料結(jié)果的狀態(tài)更變。
11.結(jié)論
在該教程中,我們介紹了一些Java8 Lambda表達式和函數(shù)表達式的最佳實踐。雖然這些新特性功能強大,但它們也是工具,每個開發(fā)者在使用時均需多加注意。