lambda與函數式

2021-03-02 Java3y
1.3 Hello,reactive world

前面兩篇文章介紹了什麼是響應式編程?和響應式流的特性,一味講概念終是枯燥,還是上手敲一敲代碼實在感受一下響應式編程的「手感」吧。

(3)lambda與函數式——響應式Spring的道法術器

這一節,我們先了解一下lambda與函數式(已經了解的朋友可以直接跳到1.3.2),熟悉一下如何使用Reactor進行響應式編程,然後使用Spring Boot2,基於Spring 5的Webflux和Reactive Spring Data逐步開發一個「Hello world」級別的RESTful service。

1.3.1 lambda與函數式

在響應式編程中,lambda與函數式的出鏡率相當高,以至於網上經常有朋友直接用「函數響應式編程」用在「響應式編程」的介紹中。這兩個詞的異同一直存在爭議,其區別雖然不像「JavaScript與Java」、「雷鋒塔與雷峰」那麼大,但隨便混用還是會顯得非常不專業:

本系列文章討論的都是「響應式編程」,關於「函數響應式編程」,你就當沒聽過,並謹慎地使用它就好了。

1.3.1.1 lambda表達式

書回正傳,為什麼響應式編程中會經常用到lambda與函數式呢?不知你對1.1.3節的一段偽代碼是否還有印象:

cartEventStream
        
        .map(cartEvent -> cartEvent.getProduct().getPrice() * cartEvent.getQuantity())
        ...

cartEventStream是一個數據流,其中的元素就是一個一個的cartEvent,map方法能夠對cartEvent進行「轉換/映射」,這裡我們將其轉換為double類型的金額。

除了轉換/映射(map)外,還有過濾(filter)、提供(supply)、消費(consume)等等針對流中元素的操作邏輯/策略,而邏輯/策略通常用方法來定義。

在Java 8之前,這就有些麻煩了。我們知道,Java是面向對象的程式語言,除了少數的原生類型外,一切都是對象。用來定義邏輯/策略的方法不能獨立存在,必須被包裝在一個對象中。比如我們比較熟悉的Comparator,其唯一的方法compare表示一種比較策略,在使用的時候,需要包裝在一個對象中傳遞給使用該策略的方法。舉例說明(源碼):

@Test
public void StudentCompareTest() {
    @Data @AllArgsConstructor class Student {   
        private int id;
        private String name;
        private double height;
        private double score;
    }

    List<Student> students = new ArrayList<>();
    students.add(new Student(10001, "張三", 1.73, 88));
    students.add(new Student(10002, "李四", 1.71, 96));
    students.add(new Student(10003, "王五", 1.85, 88));

    class StudentIdComparator<S extends Student> implements Comparator<S> { 
        @Override
        public int compare(S s1, S s2) {
            return Integer.compare(s1.getId(), s2.getId());
        }
    }

    students.sort(new StudentIdComparator<>());
    System.out.println(students);
}

@Data和@AllArgsConstructor是lombok提供的註解,能夠在編譯的字節碼中生成構造方法、getter/setter、toString等方法。依賴如下:

<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <version>1.16.20</version>
</dependency>

註:本節及以後,關於maven的依賴,可自行至maven搜索庫中查詢新的合適版本。

StudentIdComparator中固化了一種針對Student.id的比較策略,當對students進行排序的時候,將StudentIdComparator的對象傳給sort方法。輸出順序如下:

[Student(id=10001, name=張三, height=1.73, score=88.0), Student(id=10002, name=李四, height=1.71, score=96.0), Student(id=10003, name=王五, height=1.85, score=88.0)]

假設這時候我們需要對學生的身高或分數進行排序,再定義Comparator的實現類有些麻煩了,而且沒必要,「傳統」的簡化方式是直接傳入匿名內部類:

    students.sort(new Comparator<Student>() {
        @Override
        public int compare(Student s1, Student s2) {
            return Double.compare(s1.getHeight(), s2.getHeight());
        }
    });

但其實,我們會發現,無論哪種比較策略,只有compare方法內的代碼發生變化,也就是說sort方法關心的只是傳入的兩個參數Student s1, Student s2以及返回的結論return Double.compare(s1.getHeight(), s2.getHeight())這一句比較策略,何不只保留它們呢?

students.sort((Student s1, Student s2) -> {return Double.compare(s1.getHeight(), s2.getHeight());});

這樣看起來代碼就少多了。其中

(Student s1, Student s2) -> {return Double.compare(s1.getHeight(), s2.getHeight())}

就是lambda表達式,lambda表達式的語法如下:

(type1 arg1, type2 arg2...) -> { body }

->前後分別表示參數和方法體。從代碼編寫方式上來說,這就可以算作是「函數式」編程範式了,因為我們傳給sort的是一個lambda表達式的形式定義的「函數」,這個「函數」有輸入和輸出,在開發者看起來是赤裸裸的,沒有使用對象封裝起來的。

「函數式」編程範式的核心特點之一:函數是"一等公民"。
所謂"一等公民"(first class),指的是函數與其他數據類型一樣,處於平等地位,可以賦值給其他變量,也可以作為參數,傳入另一個函數,或者作為別的函數的返回值。

但也僅僅是「看起來」是「函數式」的了,Java終究是面向對象的語言,List.sort的方法定義仍然是接受一個Comparator對象作為參數的。但是一定要糾結Java是不是純正的函數式語言嗎?沒這個必要,實用至上嘛。

既然如此,問題來了,sort是如何將這個lambda「看做」一個Comparator對象的呢?

不難發現,Comparator接口僅有一個抽象方法,因此sort也就不難「推斷」lambda所定義的輸入參數和方法體表示的正是這個唯一的抽象方法compare。

1.3.1.2 函數式接口

像Comparator這樣的只有一個抽象方法的接口,叫做函數式接口(Functional Interface)。與Comparator類似,其他函數式接口的唯一的抽象方法也可以用lambda來表示。

我們看一下Comparator的源碼,發現其多了一個@FunctionalInterface的註解,用來表明它是一個函數式接口。標記了該註解的接口有且僅有一個抽象方法,否則會報編譯錯誤。

再看一下其他的僅有一個抽象方法的接口,比如Runnable和Callable,發現也都在Java 8之後加了@FunctionalInterface註解。對於Runnable來說,接口定義如下:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

不難推測,其lambda的寫法應該是 () -> { body },它不接收任何參數,方法體中也無return返回值,用起來像這樣:

new Thread(() -> {doSomething();});

此外,隨lambda一同增加的還有一個java.util.function包,其中定義了一些常見的函數式接口的。比如:

Function,接受一個輸入參數,返回一個結果。參數與返回值的類型可以不同,我們之前的map方法內的lambda就是表示這個函數式接口的;

Consumer,接受一個輸入參數並且無返回的操作。比如我們針對數據流的每一個元素進行列印,就可以用基於Consumer的lambda;

Supplier,無需輸入參數,只返回結果。看接口名就知道是發揮了對象工廠的作用;

Predicate,接受一個輸入參數,返回一個布爾值結果。比如我們在對數據流中的元素進行篩選的時候,就可以用基於Predicate的lambda;

1.3.1.3 簡化的lambda

以lambda作為參數的方法能夠推斷出來lambda所表示的是哪個函數式接口的那個抽象方法。類似地,編譯期還可以做更多的推斷。我們再回到最初的Comparator的例子並繼續簡化如下lambda表達式:

students.sort((Student s1, Student s2) -> {return Double.compare(s1.getHeight(), s2.getHeight());});

1)首先,傳入的參數類型是可以推斷出來的。因為students是以Student為元素的數組List,其sort方法自然接收Comparator<? super Student>的對象作為參數,這一切都可以通過泛型約束。

students.sort((s1, s2) -> {return Double.compare(s1.getHeight(), s2.getHeight());});

2)如果只有一個return語句的話,return和方法體的大括號都可以省略(compare方法的返回值就是lambda返回值):

students.sort((s1, s2) -> Double.compare(s1.getHeight(), s2.getHeight()));

3)注意到,Comparator接口還提供了豐富的靜態方法,比如:

public static<T> Comparator<T> comparingDouble(ToDoubleFunction<? super T> keyExtractor) {
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> Double.compare(keyExtractor.applyAsDouble(c1), keyExtractor.applyAsDouble(c2));
}

這個方法為我們包裝好了Double.compare。它接收一個返回類型為Double的函數式接口ToDoubleFunction,可以看做是Function,用lambda表示的話就是student -> student.getHeight()。

因此,我們的sort方法又可以寫作:

students.sort(Comparator.comparingDouble((student) -> student.getHeight()));

其一,對於只有一個參數的lambda來說,參數外邊的小括號可以省略:

students.sort(Comparator.comparingDouble(student -> student.getHeight()));

其二,對於僅有一個方法調用的lambda方法體來說,通常又可以用類::方法進一步簡化,以上代碼又可以進一步簡化為:

students.sort(Comparator.comparingDouble(Student::getScore));

這裡是調用參數所代表對象的某個方法,與之類似的還有比如:

使用類::方法這種寫法是不是更加有函數式的感覺了呢,似乎真是把函數作為參數傳遞給某個方法了呢~

就不再繼續舉例了,以上這些形形×××的簡化你可能會感覺難以記憶,其實無需記憶,多數IDE都能夠提供簡化建議的。

1.3.1.4 總結

在程式語言的世界裡,Java就像是一個穩健的中年人,它始終將語言的向後兼容性和穩定性放在首位,不會隨隨便便因為某種語言特性或語法糖就心動,但是對於有顯著預期收益的語言特性也會果斷出擊,泛型如此,lambda亦是如此,或許對它們的引入都不夠徹底和完美,但卻足夠實用,能夠給開發者帶來很大便利。這應該也是Java語言能夠持續保持活力的原因之一吧!

至於函數式方面更加複雜的概念,這裡就不多介紹了。

相關焦點

  • C++中的lambda函數
    作者:  2019信息與計算科學專業   楊澤天lambda函數是C++11中的匿名函數,又叫lambda表達式,叫lambda表達式更好理解,因為函數是不可以在函數中定義的,表達式可以。lambda表達式,可以簡化編程工作。
  • 通過「四不要」,掌握 Python 的 Lambda 函數
    假設您知道什麼是 lambda 函數,本文旨在提供有關如何正確使用 lambda 函數的一些常規準則。1. 不要返回任何值看看語法,您可能會注意到我們在 lambda 函數中並沒有返回任何內容。這都是因為 lambda 函數只能包含一個表達式。
  • 記住「四不要」,掌握 Python 的 Lambda 函數
    假設您知道什麼是 lambda 函數,本文旨在提供有關如何正確使用 lambda 函數的一些常規準則。1. 不要返回任何值看看語法,您可能會注意到我們在 lambda 函數中並沒有返回任何內容。這都是因為 lambda 函數只能包含一個表達式。
  • Python中的lambda函數
    ,這就是為什麼我們將它們稱為「lambda函數」。匿名函數是指沒有聲明函數名稱的函數。儘管它們在語法上看起來不同,lambda函數的行為方式與使用def關鍵字聲明的一般函數相同。以下是Python中 lambda函數的特點:在本文中,我們將詳細討論Python中的lambda函數,並演示使用它們的例子。
  • python之lambda函數使用
    一,lambda函數的概念lambda函數被稱為匿名函數,實際就是說沒有名稱的函數,形式如下:
  • 自從學會Java中的lambda表達式和函數式編程技巧,再也不用加班了!
    題記本文將分享如何在Java程序中使用lambda表達式和函數式編程技巧在Java SE 8之前,匿名類通常用於將功能傳遞給方法。這種做法混淆了原始碼,使代碼難以理解***。Java 8通過引入lambda來解決這個問題。本教程首先介紹lambda的語言特性,然後詳細的介紹了如何使用lambda表達式並根據目標類型進行函數式編程。
  • Python匿名函數:Lambda表達式
    我們以一張圖形進入主題:從圖中我們可以看出lambda表達式幾點特徵:簡潔性,符合了Python的一貫宗旨;起到了函數的作用,但未顯示函數名稱,這就是匿名函數;有形參;有返回值的。【2】Lambda表達式如何實現函數功能?
  • 一文帶你認識 C++ 中的 Lambda 函數
    因為 lambda並不總是函數指針,它一個表達式。但是為了簡單起見,我一直都稱它為函數。那麼在本文中,我將會交替使用lambda函數和表達式來稱呼。Lambda函數是一段簡短的代碼片段,它具有以下特點:(1)不值得命名(無名,匿名,可處理等,無論您如何稱呼)(2)不會重複使用換句話說,它只是語法糖。
  • Python每天一分鐘:lambda表達式 (匿名函數)及用法詳解
    lambda表達式,又稱匿名函數,是現代各種程式語言爭相引入的一種語法,其功能堪比函數,設計卻比函數簡潔。lambda 表達式可以用來替換局部函數(感興趣的讀者可以自行查閱「局部函數」),下面為大家演示lambda表達式的具體用法。lambda表達式定義首先以一個代碼例子讓大家對lambda表達式有一個直觀的認識:
  • Python中的4個Lambda函數示例
    Lambda函數式Python裡的匿名函數,有時候提到匿名函數,就是指Lambda函數,其基本語法是:lambda parameters: expression。這裡用lambda關鍵詞標記我們要定義一個Lambda函數,然後是參數列表,參數的個數可以是0個,或者多個。後面是冒號(英文狀態下),然後就是Lambda函數中的表達式。
  • 詳細講解:python中的lambda與sorted函數
    本文內容主要介紹了python中的lambda與sorted函數的相關資料,幫助大家更好的理解和學習python,感興趣的朋友可以了解下!!!
  • Kotlin函數式編程
    那麼在函數式編程中當然一切皆是函數。在Kotlin中函數式的地位和對象一樣高,你可以在方法中輸入函數,也可以返回函數。函數式編程FP特徵:函數式編程核心概念:函數是「一等公民」:是指函數與其他數據類型是一樣的,處於平等地位。函數可以作為其他函數的參數傳入,也可以作為其他函數的返回值返回。
  • python入門基礎之lambda匿名函數詳解
    python入門基礎之lambda匿名函數詳解剛開始學習python的時候很多人可能對於lambda函數不了解,感覺和def很混亂,下面我來介紹一下lambda函數我從一下幾個方面來介紹lambda:1、lambda簡介2、lambda與def不同之處3、lambda的使用方法
  • 高階函數與函數式編程
    根據程式語言理論,一等對象必須滿足以下條件:Python 函數同時滿足這幾個條件,因而也被稱為 一等函數 。高階函數 則是指那些以函數為參數,或者將函數作為結果返回的函數。對高階函數稍加利用,便能玩出很多花樣來。本節從一些典型的案例入手,講解 Python 函數高級用法。
  • Python中的函數式編程
    函數程式語言最重要的基礎是λ演算(lambda calculus)。而且λ演算的函數可以接受函數當作輸入(引數)和輸出(傳出值)。(維基百科:函數式編程)所謂編程範式(Programming paradigm)是指編程風格、方法或模式,比如面向過程編程(C語言)、面向對象編程(C++)、面向函數式編程(Haskell),並不是說某種程式語言一定屬於某種範式,例如 Python 就是多範式程式語言。
  • 函數式編程
    ,我們會看到如下函數式編程的長相:函數式編程的三大特性:immutable data 不可變數據:像Clojure一樣,默認上變量是不可變的,如果你要改變變量,你需要把變量copy出去修改。函數式編程的幾個技術map & reduce :這個技術不用多說了,函數式編程最常見的技術就是對一個集合做Map和Reduce操作。這比起過程式的語言來說,在代碼上要更容易閱讀。
  • Java lambda表達式
    Java lambda表達式是Java 8新特性。它是步入Java函數式編程的第一步。因此,Java lambda表達式是創建時不屬於任何類的函數。它可以像一個對象一樣傳遞,並按要求執行。Java Lambdas和單一方法接口函數式編程通常用於實現事件監聽器。Java中的事件監聽器通常被定義為帶有單個方法的Java接口。
  • 「數據清洗」lambda表達式配合使用的四種函數
    標籤:數據清洗、pythonlambda表達式配合使用的四種函數一、什麼是lambda表達式基本特性使用方法filter函數map函數sorted函數reduce函數總結什麼是lambda表達式>lambda 表達式是一個匿名函數,lambda表達式基於數學中的λ演算得名,直接對應於其中的lambda抽象,是一個匿名函數,即沒有函數名的函數。
  • 白話 Python 的函數式編程
    所謂函數式編程,就是指代碼中每一塊都是不可變的(immutable),都是由 pure function 的形式組成。這裡的 pure function 是指函數本身相互獨立,互不影響,對於相同的輸入,總會有相同的輸出。也就是我們常說的沒有副作用。
  • Python中的Lambda表達式
    Lambda函數的行為就像使用def關鍵字聲明的常規函數一樣。當您希望以簡潔的方式定義一個小型函數時,它們就派上用場了。它們只能包含一個表達式,因此不適合具有控制流語句的函數。Lambda函數的語法lambda arguments: expressionLambda函數可以具有任意數量的參數,但只能有一個表達式。