javascript
(3)lambda与函数式——响应式Spring的道法术器
本系列文章索引:《響應(yīng)式Spring的道法術(shù)器》
前情提要: 什么是響應(yīng)式編程 | 響應(yīng)式流
本文源碼
1.3 Hello,reactive world
前面兩篇文章介紹了響應(yīng)式編程和響應(yīng)式流的特性,一味講概念終是枯燥,還是上手敲一敲代碼實(shí)在感受一下響應(yīng)式編程的“手感”吧。
這一節(jié),我們先了解一下lambda與函數(shù)式(已經(jīng)了解的朋友可以直接跳到1.3.2),熟悉一下如何使用Reactor進(jìn)行響應(yīng)式編程,然后使用Spring Boot2,基于Spring 5的Webflux和Reactive Spring Data逐步開發(fā)一個(gè)“Hello world”級(jí)別的RESTful service。
1.3.1 lambda與函數(shù)式
在響應(yīng)式編程中,lambda與函數(shù)式的出鏡率相當(dāng)高,以至于網(wǎng)上經(jīng)常有朋友直接用“函數(shù)響應(yīng)式編程”用在“響應(yīng)式編程”的介紹中。這兩個(gè)詞的異同一直存在爭(zhēng)議,其區(qū)別雖然不像“JavaScript與Java”、“雷鋒塔與雷峰”那么大,但隨便混用還是會(huì)顯得非常不專業(yè):
- 函數(shù)響應(yīng)式編程的重點(diǎn)在于“函數(shù)式”的語(yǔ)言特性,這個(gè)概念在二十年前就蓋棺定論了。
- 響應(yīng)式編程的重點(diǎn)在于“基于事件流”的異步編程范式,由不斷產(chǎn)生的數(shù)據(jù)/時(shí)間來推動(dòng)邏輯的執(zhí)行。
本系列文章討論的都是“響應(yīng)式編程”,關(guān)于“函數(shù)響應(yīng)式編程”,你就當(dāng)沒聽過,并謹(jǐn)慎地使用它就好了。
1.3.1.1 lambda表達(dá)式
書回正傳,為什么響應(yīng)式編程中會(huì)經(jīng)常用到lambda與函數(shù)式呢?不知你對(duì)1.1.3節(jié)的一段偽代碼是否還有印象:
cartEventStream// 分別計(jì)算商品金額.map(cartEvent -> cartEvent.getProduct().getPrice() * cartEvent.getQuantity())...cartEventStream是一個(gè)數(shù)據(jù)流,其中的元素就是一個(gè)一個(gè)的cartEvent,map方法能夠?qū)artEvent進(jìn)行“轉(zhuǎn)換/映射”,這里我們將其轉(zhuǎn)換為double類型的金額。
除了轉(zhuǎn)換/映射(map)外,還有過濾(filter)、提供(supply)、消費(fèi)(consume)等等針對(duì)流中元素的操作邏輯/策略,而邏輯/策略通常用方法來定義。
在Java 8之前,這就有些麻煩了。我們知道,Java是面向?qū)ο蟮木幊陶Z(yǔ)言,除了少數(shù)的原生類型外,一切都是對(duì)象。用來定義邏輯/策略的方法不能獨(dú)立存在,必須被包裝在一個(gè)對(duì)象中。比如我們比較熟悉的Comparator,其唯一的方法compare表示一種比較策略,在使用的時(shí)候,需要包裝在一個(gè)對(duì)象中傳遞給使用該策略的方法。舉例說明(源碼):
@Test public void StudentCompareTest() {@Data @AllArgsConstructor class Student { // 1private 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> { // 2@Overridepublic int compare(S s1, S s2) {return Integer.compare(s1.getId(), s2.getId());}}students.sort(new StudentIdComparator<>());System.out.println(students); }@Data和@AllArgsConstructor是lombok提供的注解,能夠在編譯的字節(jié)碼中生成構(gòu)造方法、getter/setter、toString等方法。依賴如下:
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.16.20</version> </dependency>注:本節(jié)及以后,關(guān)于maven的依賴,可自行至maven搜索庫(kù)中查詢新的合適版本。
StudentIdComparator中固化了一種針對(duì)Student.id的比較策略,當(dāng)對(duì)students進(jìn)行排序的時(shí)候,將StudentIdComparator的對(duì)象傳給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)]
假設(shè)這時(shí)候我們需要對(duì)學(xué)生的身高或分?jǐn)?shù)進(jìn)行排序,再定義Comparator的實(shí)現(xiàn)類有些麻煩了,而且沒必要,“傳統(tǒng)”的簡(jiǎn)化方式是直接傳入匿名內(nèi)部類:
students.sort(new Comparator<Student>() {@Overridepublic int compare(Student s1, Student s2) {return Double.compare(s1.getHeight(), s2.getHeight());}});但其實(shí),我們會(huì)發(fā)現(xiàn),無(wú)論哪種比較策略,只有compare方法內(nèi)的代碼發(fā)生變化,也就是說sort方法關(guān)心的只是傳入的兩個(gè)參數(shù)Student s1, Student s2以及返回的結(jié)論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表達(dá)式,lambda表達(dá)式的語(yǔ)法如下:
(type1 arg1, type2 arg2...) -> { body }->前后分別表示參數(shù)和方法體。從代碼編寫方式上來說,這就可以算作是“函數(shù)式”編程范式了,因?yàn)槲覀儌鹘osort的是一個(gè)lambda表達(dá)式的形式定義的“函數(shù)”,這個(gè)“函數(shù)”有輸入和輸出,在開發(fā)者看起來是赤裸裸的,沒有使用對(duì)象封裝起來的。
“函數(shù)式”編程范式的核心特點(diǎn)之一:函數(shù)是"一等公民"。
所謂"一等公民"(first class),指的是函數(shù)與其他數(shù)據(jù)類型一樣,處于平等地位,可以賦值給其他變量,也可以作為參數(shù),傳入另一個(gè)函數(shù),或者作為別的函數(shù)的返回值。
但也僅僅是“看起來”是“函數(shù)式”的了,Java終究是面向?qū)ο蟮恼Z(yǔ)言,List.sort的方法定義仍然是接受一個(gè)Comparator對(duì)象作為參數(shù)的。但是一定要糾結(jié)Java是不是純正的函數(shù)式語(yǔ)言嗎?沒這個(gè)必要,實(shí)用至上嘛。
既然如此,問題來了,sort是如何將這個(gè)lambda“看做”一個(gè)Comparator對(duì)象的呢?
不難發(fā)現(xiàn),Comparator接口僅有一個(gè)抽象方法,因此sort也就不難“推斷”lambda所定義的輸入?yún)?shù)和方法體表示的正是這個(gè)唯一的抽象方法compare。
1.3.1.2 函數(shù)式接口
像Comparator這樣的只有一個(gè)抽象方法的接口,叫做函數(shù)式接口(Functional Interface)。與Comparator類似,其他函數(shù)式接口的唯一的抽象方法也可以用lambda來表示。
我們看一下Comparator的源碼,發(fā)現(xiàn)其多了一個(gè)@FunctionalInterface的注解,用來表明它是一個(gè)函數(shù)式接口。標(biāo)記了該注解的接口有且僅有一個(gè)抽象方法,否則會(huì)報(bào)編譯錯(cuò)誤。
再看一下其他的僅有一個(gè)抽象方法的接口,比如Runnable和Callable,發(fā)現(xiàn)也都在Java 8之后加了@FunctionalInterface注解。對(duì)于Runnable來說,接口定義如下:
@FunctionalInterface public interface Runnable {public abstract void run(); }不難推測(cè),其lambda的寫法應(yīng)該是 () -> { body },它不接收任何參數(shù),方法體中也無(wú)return返回值,用起來像這樣:
new Thread(() -> {doSomething();});此外,隨lambda一同增加的還有一個(gè)java.util.function包,其中定義了一些常見的函數(shù)式接口的。比如:
- Function,接受一個(gè)輸入?yún)?shù),返回一個(gè)結(jié)果。參數(shù)與返回值的類型可以不同,我們之前的map方法內(nèi)的lambda就是表示這個(gè)函數(shù)式接口的;
- Consumer,接受一個(gè)輸入?yún)?shù)并且無(wú)返回的操作。比如我們針對(duì)數(shù)據(jù)流的每一個(gè)元素進(jìn)行打印,就可以用基于Consumer的lambda;
- Supplier,無(wú)需輸入?yún)?shù),只返回結(jié)果。看接口名就知道是發(fā)揮了對(duì)象工廠的作用;
- Predicate,接受一個(gè)輸入?yún)?shù),返回一個(gè)布爾值結(jié)果。比如我們?cè)趯?duì)數(shù)據(jù)流中的元素進(jìn)行篩選的時(shí)候,就可以用基于Predicate的lambda;
- ...
1.3.1.3 簡(jiǎn)化的lambda
以lambda作為參數(shù)的方法能夠推斷出來lambda所表示的是哪個(gè)函數(shù)式接口的那個(gè)抽象方法。類似地,編譯期還可以做更多的推斷。我們?cè)倩氐阶畛醯腃omparator的例子并繼續(xù)簡(jiǎn)化如下lambda表達(dá)式:
students.sort((Student s1, Student s2) -> {return Double.compare(s1.getHeight(), s2.getHeight());});1)首先,傳入的參數(shù)類型是可以推斷出來的。因?yàn)閟tudents是以Student為元素的數(shù)組List<Student>,其sort方法自然接收Comparator<? super Student>的對(duì)象作為參數(shù),這一切都可以通過泛型約束。
students.sort((s1, s2) -> {return Double.compare(s1.getHeight(), s2.getHeight());});2)如果只有一個(gè)return語(yǔ)句的話,return和方法體的大括號(hào)都可以省略(compare方法的返回值就是lambda返回值):
students.sort((s1, s2) -> Double.compare(s1.getHeight(), s2.getHeight()));3)注意到,Comparator接口還提供了豐富的靜態(tài)方法,比如:
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)); }這個(gè)方法為我們包裝好了Double.compare。它接收一個(gè)返回類型為Double的函數(shù)式接口ToDoubleFunction<? super T>,可以看做是Function<? super T, Double>,用lambda表示的話就是student -> student.getHeight()。
因此,我們的sort方法又可以寫作:
students.sort(Comparator.comparingDouble((student) -> student.getHeight()));其一,對(duì)于只有一個(gè)參數(shù)的lambda來說,參數(shù)外邊的小括號(hào)可以省略:
students.sort(Comparator.comparingDouble(student -> student.getHeight()));其二,對(duì)于僅有一個(gè)方法調(diào)用的lambda方法體來說,通常又可以用類::方法進(jìn)一步簡(jiǎn)化,以上代碼又可以進(jìn)一步簡(jiǎn)化為:
students.sort(Comparator.comparingDouble(Student::getScore));這里是調(diào)用參數(shù)所代表對(duì)象的某個(gè)方法,與之類似的還有比如:
- string -> System.out.println(string),可以簡(jiǎn)化為System.out::println,這里是將參數(shù)作為System.out::println的參數(shù)了;
- () -> new HashMap<>(),可以簡(jiǎn)化為HashMap::new,這里沒有參數(shù),也可以進(jìn)行簡(jiǎn)化。
使用類::方法這種寫法是不是更加有函數(shù)式的感覺了呢,似乎真是把函數(shù)作為參數(shù)傳遞給某個(gè)方法了呢~
就不再繼續(xù)舉例了,以上這些形形×××的簡(jiǎn)化你可能會(huì)感覺難以記憶,其實(shí)無(wú)需記憶,多數(shù)IDE都能夠提供簡(jiǎn)化建議的。
1.3.1.4 總結(jié)
在編程語(yǔ)言的世界里,Java就像是一個(gè)穩(wěn)健的中年人,它始終將語(yǔ)言的向后兼容性和穩(wěn)定性放在首位,不會(huì)隨隨便便因?yàn)槟撤N語(yǔ)言特性或語(yǔ)法糖就心動(dòng),但是對(duì)于有顯著預(yù)期收益的語(yǔ)言特性也會(huì)果斷出擊,泛型如此,lambda亦是如此,或許對(duì)它們的引入都不夠徹底和完美,但卻足夠?qū)嵱?#xff0c;能夠給開發(fā)者帶來很大便利。這應(yīng)該也是Java語(yǔ)言能夠持續(xù)保持活力的原因之一吧!
至于函數(shù)式方面更加復(fù)雜的概念,這里就不多介紹了。下面我們就認(rèn)識(shí)一下Reactor吧~
轉(zhuǎn)載于:https://blog.51cto.com/liukang/2090187
總結(jié)
以上是生活随笔為你收集整理的(3)lambda与函数式——响应式Spring的道法术器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 根据文件名获取文件扩展名
- 下一篇: [后缀自动机][树形DP] BZOJ 4