深入理解 Java 中的 Lambda

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

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

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

方法類型

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

  • 可以作為參數進行傳遞,
  • 可以作為方法的返回值
  • 可以賦值給一個變量.

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

  • 一種內建類型 (比如 int 或者 double)
  • 一個類 (比如ArrayList)
  • 一個接口 (比如 Iterable)

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

class LambdaMap { static void oneStringArgumentMethod(String arg) { System.out.println(arg); }}

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

  • 它必須包含一個名為apply的函數。
  • 函數返回類型必須是void。
  • 函數必須接受一個String類型可以轉換到的對象的參數。

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

interface OneStringArgumentInterface { void apply(String arg);}

利用這個接口,函數可以分配給一個變量:

OneStringArgumentInterface meth = LambdaMap::oneStringArgumentMethod;

用這種方法使用接口作為類型,函數可以藉此被分配給變量,傳遞參數並且從函數返回:

static OneStringArgumentInterface getWriter() { return LambdaMap::oneStringArgumentMethod;}static void write(OneStringArgumentInterface writer, String msg) { writer.apply(msg);}

最終函數是一等公民。

泛型函數類型

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

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

interface OneArgumentInterface { void apply(T arg);}

OneStringArgumentInterface函數可以被分配給它:

OneArgumentInterface meth = LambdaMap::oneStringArgumentMethod;

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

static  void applyArgument(OneArgumentInterface meth, T arg) { meth.apply(arg);} 

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

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

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

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

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

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

這看起來簡潔明瞭 - 請注意 Lambda 表達式如何缺省任何明確的類型信息。 沒有顯式模板實例化,沒有方法簽名。

結語

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

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


分享到:


相關文章: