深入理解 Java 中的 Lambda

airland發表於2021-09-09

我花了相當多的閱讀和編碼時間才最終理解Java Lambdas如何在概念上正常工作的。我閱讀的大多數教程和介紹都遵循自頂向下的方法,從用例開始,最後以概念性問題結束。在這篇文章中,我想提供一個自下而上的解釋,從其他已建立的Java概念中推匯出Lambdas的概念。

首先介紹下方法的型別化,這是支援方法作為一流公民的先決條件。基於此,Lambdas的概念是被以匿名類用法的進化和特例提出的。所有這一切都透過實現和使用  來說明。

這篇文章的主要受眾是那些已掌握函數語言程式設計基礎的人,以及那些想從概念上理解Lambdas如何嵌入Java語言的人。

方法型別

從Java 8起方法就是  了。 按照標準的定義,程式語言中的一等公民是一個具有下列功能的實體,

可以作為引數進行傳遞,

可以作為方法的返回值

可以賦值給一個變數.

在Java中,每一個引數、返回值或變數都是有型別的,因此每個一等公民都必須是有型別的。Java中的一種型別可以是以下內容之一:

一種內建型別 (比如 int 或者 double)

一個類 (比如ArrayList)

一個介面 (比如 Iterable)

方法是透過介面進行定義型別的。 它們不隱式的實現特定介面,但是在必要的時候,如果一個方法符合一個介面,那麼在編譯期間,Java編譯器會對其進行隱式的檢查。 舉個例子說明:

classLambdaMap{staticvoidoneStringArgumentMethod(String arg){        System.out.println(arg);    }}

關於oneStringArgumentMethod函式的型別,與之相關的有:它的的函式是靜態的,返回型別是void,它接受一個String型別的引數。一個靜態函式符合包含一個apply函式的介面,apply函式的簽名相應地符合這個靜態函式的簽名。oneStringArgumentMethod函式對應的介面因此必須符合下列標準。

它必須包含一個名為apply的函式。

函式返回型別必須是void。

函式必須接受一個String型別可以轉換到的物件的引數。

在符合這個標準的介面之中,下面的這個是最明確的:

interfaceOneStringArgumentInterface{voidapply(Stringarg);}

利用這個介面,函式可以分配給一個變數:

OneStringArgumentInterfacemeth = LambdaMap::oneStringArgumentMethod;

用這種方法使用介面作為型別,函式可以藉此被分配給變數,傳遞引數並且從函式返回:

staticOneStringArgumentInterfacegetWriter(){returnLambdaMap::oneStringArgumentMethod;}staticvoidwrite(OneStringArgumentInterface writer, String msg){    writer.apply(msg);}

最終函式是一等公民。

泛型函式型別

就像使用集合一樣,泛型為函式型別增加了大量的功能和靈活性。實現功能上的演算法而不考慮型別相關資訊,泛型函式型別使其變為可能。在對map函式的實現中,會在下面用到這種功能。

在這提供的OneStringArgumentInterface一個泛型版本:

interfaceOneArgumentInterface{voidapply(T arg);}

OneStringArgumentInterface函式可以被分配給它:

OneArgumentInterface meth = LambdaMap::oneStringArgumentMethod;

透過使用泛型函式型別,它現在可以以一種通用的方法實現演算法,就像它在集合中使用的一樣:

staticvoidapplyArgument(OneArgumentInterface meth, T arg){    meth.apply(arg);}

上面的函式並沒有什麼用,然而它至少可以提出一個想法:對函式作為第一個類成員的支援怎樣可以形成非常簡潔且靈活的程式碼:

applyArgument(Lambda::oneStringArgumentMethod,"X");

實現map

在諸多高階函式中,map是最經典的. map的第一個引數是函式,該函式可以接收一個引數並返回一個值;第二個引數是值列表. map使用傳入的函式處理值列表的每一項,然後返回一個新的值列表。下面Python的程式碼片段,可以很好的說明map的用法:

>>>map(math.sqrt,[1, 4, 9, 16])[1.0, 2.0, 3.0, 4.0]

在本節的後續內容中,將給出該函式的Java實現。Java 8已經透過Stream提供了該函式。因為主要出於教學目的,所以,本節中給出的實現特意保持簡單,僅限於List物件使用。

與Python不同, 在Java中 必須 首先考慮map第一個引數的型別:一個可以接收一個引數並返回一個值的方法。引數的型別和返回值的型別可以不同。下面介面符合這個預期,顯然,I表示引數(入參),O表示返回值(出參):

interfaceMapFunction{    O apply(Iin);}

泛型map方法的實現,變得驚人的簡單明瞭:

staticListmap(MapFunctionfunc,Listinput){List out = newArrayList();for(Iin: input) {        out.add(func.apply(in));    }returnout;}

建立新的返回值列表out(用於儲存O型別的物件).

透過遍歷input,func處理列表的每一項,並將返回值新增到out中。

返回out.

下面是實際使用map方法的例項:

MapFunctionfunc=Math::sqrt;Listoutput=map(func, Arrays.asList(1.,4.,9.,16.));System.out.println(output);

在Python one-liner的推動下,可以用更簡潔的方法表達:

System.out.println(map(Math::sqrt,Arrays.asList(1., 4., 9., 16.)));

Java畢竟不是Python...

Lambdas來了!

讀者可能會注意到,還沒有提到Lambdas。這是由於採用了“自下而上”的方式描述,現在基礎已基本建立,Lambdas將在後續的章節中介紹。

下面的用例作為基礎:一個double型別的list,表示半徑,然後得到一個列表,表示圓面積。map方法就是為此任務預先準備的。計算圓面積的公式是眾所周知的:

A = r 2 π

應用這個公式的方法很容易實現:

staticDoublecircleArea(Doubleradius) {returnMath.pow(radius,2) * Math.PI;}

這個方法現在可以用作map方法的第一個引數:

System.out.println(map(LambdaMap::circleArea,Arrays.asList(1., 4., 9., 16.)));

如果circleArea方法只需要這一次, 沒有道理把類介面被他弄得亂七八糟,也沒有道理將實現和真正使用它的地方分離。最佳實踐是使用用匿名內部類。可以看到,例項化一個實現MapFunction介面的匿名內部類可以很好的完成這個任務:

System.out.println(        map(newMapFunction() {publicDoubleapply(Doubleradius) {returnMath.sqrt(radius) * Math.PI;                }            },            Arrays.asList(1.,2.,3.,4.)));

這看起來很漂亮,但是很多人會認為函式式的解決方案更清晰,更具可讀性:

Listout= new ArrayList();for(Doubleradius : Arrays.asList(1.,2.,3.,4.)) {out.add(Math.sqrt(radius) * Math.PI);}System.out.println(out);

到目前為止,最後是使用Lambda表示式。 讀者應該注意Lambda如何取代上面提到的匿名類:

System.out.println(map(radius -> {returnMath.sqrt(radius) * Math.PI; },            Arrays.asList(1.,2.,3.,4.)));

這看起來簡潔明瞭 - 請注意 Lambda 表示式如何預設任何明確的型別資訊。 沒有顯式模板例項化,沒有方法簽名。

Lambda表示式由兩部分組成,這兩部分被->分隔。第一部分是引數列表,第二部分是實際實現。

Lambda表示式和匿名內部類作用完全相同,然而它摒棄了許多編譯器可以自動推斷的樣板程式碼。讓我們再次比較這兩種方式,然後分析編譯器為開發人員節省了哪些工作。

MapFunction functionLambda =        radius -> Math.sqrt(radius) * Math.PI;MapFunction functionClass =        new MapFunction() {publicDoubleapply(Doubleradius) {returnMath.sqrt(radius) * Math.PI;            }        };

對於Lambda實現來說,只有一個表示式,返回語句和花括號可以省略。這使得程式碼更簡短。

Lambda表示式的返回值型別是從Lambda實現推薦出來的。

對於引數型別,我不完全確定,但我認為必須從Lambda表示式所處的上下文中推斷出引數型別。

最後編譯器必須檢查返回值型別 是否 與Lambda的上下文匹配,以及引數型別是否與Lambda實現匹配。

這一切都可以在編譯期間完成,根本沒有執行時開銷。

總而言之,Java中的Lambdas的概念是整潔的。我支援編寫更簡潔、更清晰的程式碼,並讓程式設計師免於編寫可由編譯器自動推斷的架手架程式碼。它是語法糖,如上所述,它只不過是使用匿名類也能實現的功能。然而,我會說它是非常甜的語法糖。

另一方面,Lambdas還支援更加混淆以及難以除錯的程式碼。Python社群很早就意識到了這一點 - 雖然Python也有Lambda,但它若被廣泛使用則通常被認為是不好的風格(當巢狀函式可以被使用時,它並不難於規避)。對於Java來說,我會給出類似的建議。毫無疑問,在某些情況下,使用Lambdas會導致程式碼大大縮減並更易讀,尤其在與流有關時。在其他情況下,如果採取更保守的做法和最佳實踐,另外一種方法可能會是更好的替代。

          

作者:歐陽海陽
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/964/viewspace-2804205/,如需轉載,請註明出處,否則將追究法律責任。

相關文章