1. 程式人生 > >深入理解 Java 中的 Lambda

深入理解 Java 中的 Lambda

作者:標籤: 李三石
來源:my.oschina.net/leili

我花了相當多的閱讀和編碼時間才最終理解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<T{
    void apply(T arg);
}

OneStringArgumentInterface函式可以被分配給它:

OneArgumentInterface<String> meth = LambdaMap::oneStringArgumentMethod;

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

static <T> void applyArgument(OneArgumentInterface<T> 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表示返回值(出參):

interface MapFunction<IO{
    apply(I in);
}

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

static <I, O> List<O> map(MapFunction<I, O> func, List<I> input{
    List<O> out = new ArrayList<>();

    for (I in : input) {
        out.add(func.apply(in));
    }

    return out;
}
  1. 建立新的返回值列表out(用於儲存O型別的物件).
  2. 通過遍歷input,func處理列表的每一項,並將返回值新增到out中。
  3. 返回out.

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

MapFunction<Integer, Double> func = Math::sqrt;

List<Doubleoutput = map(func, Arrays.asList(1., 4., 9., 16.));
System.out.println(output);

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

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

Java畢竟不是Python…

Lambdas來了!

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

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

A = r2π

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

static Double circleArea(Double radius) {
    return Math.pow(radius, 2) * Math.PI;
}

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

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

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

System.out.println(
        map(new MapFunction<DoubleDouble>() {
                public Double apply(Double radius) {
                    return Math.sqrt(radius) * Math.PI;
                }
            },
            Arrays.asList(1.2.3.4.)));

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

List<Double> 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 表示式如何預設任何明確的型別資訊。 沒有顯式模板例項化,沒有方法簽名。

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

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

MapFunction<DoubleDouble> functionLambda =
        radius -> Math.sqrt(radius) * Math.PI;

MapFunction<DoubleDouble> functionClass =
        new MapFunction<DoubleDouble>() {
            public Double apply(Double radius) {
                return Math.sqrt(radius) * Math.PI;
            }
        };
  • 對於Lambda實現來說,只有一個表示式,返回語句和花括號可以省略。這使得程式碼更簡短。
  • Lambda表示式的返回值型別是從Lambda實現推斷出來的。
  • 對於引數型別,我不完全確定,但我認為必須從Lambda表示式所處的上下文中推斷出引數型別。
  • 最後編譯器必須檢查返回值型別是否與Lambda的上下文匹配,以及引數型別是否與Lambda實現匹配。

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

結語

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

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

相關推薦

深入理解 Java Lambda

作者:標籤: 李三石 來源:my.oschina.net/leili我花了相當多的閱讀和編碼時間才最終理解Java Lambdas如何在概念上正常工作的。我閱讀的大多數教程和介紹都遵循自頂向下的方法,從用例開始,最後以概念性問題結束。在這篇文章中,我想提供一個自下而上的解

JDK學習---深入理解java的String

test bound test6 -h 很多 lai 靈活性 圖形 會有 本文參考資料: 1、《深入理解jvm虛擬機》 2、《大話數據結構》、《大化設計模式》 3、http://www.cnblogs.com/ITtangtang/p/3976820.html#344102

深入理解 Java的 流 (Stream)

重要 抽象 bool sta 也會 簡單 throws image true 首先,流是什麽? 流是個抽象的概念,是對輸入輸出設備的抽象,Java程序中,對於數據的輸入/輸出操作都是以“流”的方式進行。設備可以是文件,網絡,內存等。 流具有方向性,至於是輸入流還是輸出流則

深入理解Java的逃逸分析

end 代碼 堆內存 解決 永遠 例子 append 解釋器 return 在Java的編譯體系中,一個Java的源代碼文件變成計算機可執行的機器指令的過程中,需要經過兩段編譯,第一段是把.java文件轉換成.class文件。第二段編譯是把.class轉換成機器指令的過程。

深入理解Java的字段與屬性的區別

ring rgs name 私有變量 pub tail 博文 們的 方式 轉載出處 http://blog.csdn.net/chenchunlin526/article/details/69939337 1、Java中的屬性和字段有什麽區別? 答:Java中的屬性(p

深入理解JAVA的NIO

類文件 邏輯 不同的 字節轉換 實現類 讀取 組件 進行 大量 前言: 傳統的 IO 流還是有很多缺陷的,尤其它的阻塞性加上磁盤讀寫本來就慢,會導致 CPU 使用效率大大降低。 所以,jdk 1.4 發布了 NIO 包,NIO 的文件讀寫設計顛覆了傳統 IO 的設計,采用通

深入理解Java的底層阻塞原理及實現

更多 安全 posix pla static events time() 方便 原理 談到阻塞,相信大家都不會陌生了。阻塞的應用場景真的多得不要不要的,比如 生產-消費模式,限流統計等等。什麽 ArrayBlockingQueue、 LinkedBlockingQueue、

深入理解Java的volatile關鍵字

語言 重新 為什麽 設置 模型 可見性 會有 普通 enter 在再有人問你Java內存模型是什麽,就把這篇文章發給他中我們曾經介紹過,Java語言為了解決並發編程中存在的原子性、可見性和有序性問題,提供了一系列和並發處理相關的關鍵字,比如synchronized、vola

深入理解Java的同步靜態方法和synchronized(class)程式碼塊的類鎖 深入理解Java併發synchronized同步化的程式碼塊不是this物件時的操作

一.回顧學習內容  在前面幾篇部落格中我我們已經理解了synchronized物件鎖、物件鎖的重入、synchronized方法塊、synchronized非本物件的程式碼塊,  連結:https://www.cnblogs.com/SAM-CJM/category/1314992.h

深入理解Java的synchronized鎖重入

問題匯入:如果一個執行緒呼叫了一個物件的同步方法,那麼他還能不能在呼叫這個物件的另外一個同步方法呢? 這裡就是synchronized鎖重入問題。 一.synchronized鎖重入  來看下面的程式碼: .這個是三個同步方法的類 public class Syn

深入理解Java停止執行緒

一.停止執行緒會帶來什麼? 對於單執行緒中,停止單執行緒就是直接使用關鍵字return或者break,但是在停止多執行緒時是讓執行緒在完成任務前去開啟另外一條執行緒,必須放棄當前任務,而這個過程是不可預測,所以必須去做好防備。 二.認識停止執行緒的幾個方法  2.1三個被棄用的方法 &n

深入理解Java的fail-fast和fail-safe

什麼是快速失敗(fail-fast)和安全失敗(fail-safe)?它們又和什麼內容有關係。以上兩點就是這篇文章的內容,廢話不多話,正文請慢用。 我們都接觸 HashMap、ArrayList 這些集合類,這些在 java.util 包的集合類就都是快速失敗的;而  java.ut

深入理解 Java 的 try-with-resource

背景 眾所周知,所有被開啟的系統資源,比如流、檔案或者Socket連線等,都需要被開發者手動關閉,否則隨著程式的不斷執行,資源洩露將會累積成重大的生產事故。 在Java的江湖中,存在著一種名為finally的功夫,它可以保證當你習武走火入魔之時,還可以

深入理解java的介面 (Interface)

概念 我們知道java中是單繼承的,這就有了很多的限制,比如我們需要多繼承的時候但是不能用多繼承,這時候怎麼辦呢?就引入了介面的概念,它彌補了java中單繼承的缺陷,這一點非常的好,如果要約定子類的實現要求並避免單繼承侷限就需要使用介面。 那麼什麼是介面呢?

深入理解Java的String

一、String類 想要了解一個類,最好的辦法就是看這個類的實現原始碼,來看一下String類的原始碼: public final class String implements java.io.Serializable, Comparable<Stri

深入理解Java的同步靜態方法和synchronized(class)程式碼塊的類鎖

一.回顧學習內容  在前面幾篇部落格中我我們已經理解了synchronized物件鎖、物件鎖的重入、synchronized方法塊、synchronized非本物件的程式碼塊,  我們來總結一下,上面幾篇講到內容:  1.建立執行緒類的兩個方式:繼承Thread類和實現Runable介面。  2.瞭解了Th

深入理解Java的欄位與屬性的區別

1、Java中的屬性和欄位有什麼區別?  答:Java中的屬性(property),通常可以理解為get和set方法。 而欄位(field),通常叫做“類成員”,或 "類成員變數”,有時也叫“域”,理解為“資料成員”,用來承載資料的。 這兩個概念是完全不同的。 2、屬性

深入理解Java 8 Lambda(語言篇)

原文連結:http://lucida.me/blog/java-8-lambdas-insideout-language-features/ 關於 本文介紹了 Java SE 8 中新引入的 lambda 語言特性以及這些特性背後的設計思想。這些特性包括: la

深入理解Java為什麼內部類可以訪問外部類的成員

{ final Outer this$0; flags: ACC_FINAL, ACC_SYNTHETIC Outer$Inner(Outer); flags: Code: stack=2, locals=2, args_size=2 0: a

通過ArrayList原始碼深入理解javaIterator迭代器的實現原理

注意:本文將著重從原始碼的角度對Iterator的實現進行講解,不討論List與Iterator介面的具體使用方法。不過看懂原始碼後,使用也就不是什麼問題了。 java中各種實現Iterator的類所具體使用的實現方法各不相同,但是都大同小異。因此本文將只通過