關(guān)于Lambda表達式與函數(shù)式接口的技巧與最佳實踐

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ā)者在使用時均需多加注意。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容