(3)lambda与函数式——响应式Spring的道法术器

本系列文章索引:《响应式Spring的道法术器》
前情提要: 什么是响应式编程 | 响应式流
本文源码

1.3 Hello,reactive world

前面两篇文章介绍了响应式编程响应式流的特性,一味讲概念终是枯燥,还是上手敲一敲代码实在感受一下响应式编程的“手感”吧。

这一节,我们先了解一下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是一个数据流,其中的元素就是一个一个的cartEventmap方法能够对cartEvent进行“转换/映射”,这里我们将其转换为double类型的金额。

除了转换/映射(map)外,还有过滤(filter)、提供(supply)、消费(consume)等等针对流中元素的操作逻辑/策略,而逻辑/策略通常用方法来定义。

在Java 8之前,这就有些麻烦了。我们知道,Java是面向对象的编程语言,除了少数的原生类型外,一切都是对象。用来定义逻辑/策略的方法不能独立存在,必须被包装在一个对象中。比如我们比较熟悉的Comparator,其唯一的方法compare表示一种比较策略,在使用的时候,需要包装在一个对象中传递给使用该策略的方法。举例说明(源码):

@Test
public void StudentCompareTest() {
    @Data @AllArgsConstructor class Student {   // 1
        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> { // 2
        @Override
        public int compare(S s1, S s2) {
            return Integer.compare(s1.getId(), s2.getId());
        }
    }

    students.sort(new StudentIdComparator<>());
    System.out.println(students);
}
  1. @Data@AllArgsConstructor是lombok提供的注解,能够在编译的字节码中生成构造方法、getter/setter、toString等方法。依赖如下:

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

注:本节及以后,关于maven的依赖,可自行至maven搜索库中查询新的合适版本。

  1. 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) -&gt; {return Double.compare(s1.getHeight(), s2.getHeight())}就是lambda表达式,lambda表达式的语法如下:

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

-&gt;前后分别表示参数和方法体。从代码编写方式上来说,这就可以算作是“函数式”编程范式了,因为我们传给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的注解,用来表明它是一个函数式接口。标记了该注解的接口有且仅有一个抽象方法,否则会报编译错误。

再看一下其他的仅有一个抽象方法的接口,比如RunnableCallable,发现也都在Java 8之后加了@FunctionalInterface注解。对于Runnable来说,接口定义如下:

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

不难推测,其lambda的写法应该是 () -&gt; { 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&lt;Student&gt;,其sort方法自然接收Comparator&lt;? super Student&gt;的对象作为参数,这一切都可以通过泛型约束。

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&lt;? super T&gt;,可以看做是Function&lt;? super T, Double&gt;,用lambda表示的话就是student -&gt; 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));

这里是调用参数所代表对象的某个方法,与之类似的还有比如:

  • string -&gt; System.out.println(string),可以简化为System.out::println,这里是将参数作为System.out::println的参数了;
  • () -&gt; new HashMap&lt;&gt;(),可以简化为HashMap::new,这里没有参数,也可以进行简化。

使用类::方法这种写法是不是更加有函数式的感觉了呢,似乎真是把函数作为参数传递给某个方法了呢~

就不再继续举例了,以上这些形形×××的简化你可能会感觉难以记忆,其实无需记忆,多数IDE都能够提供简化建议的。

1.3.1.4 总结

在编程语言的世界里,Java就像是一个稳健的中年人,它始终将语言的向后兼容性和稳定性放在首位,不会随随便便因为某种语言特性或语法糖就心动,但是对于有显著预期收益的语言特性也会果断出击,泛型如此,lambda亦是如此,或许对它们的引入都不够彻底和完美,但却足够实用,能够给开发者带来很大便利。这应该也是Java语言能够持续保持活力的原因之一吧!

至于函数式方面更加复杂的概念,这里就不多介绍了。下面我们就认识一下Reactor吧~

原文地址:http://blog.51cto.com/liukang/2090187

时间: 2024-11-06 07:38:53

(3)lambda与函数式——响应式Spring的道法术器的相关文章

响应式Spring的道法术器(Spring WebFlux 快速上手 + 全面介绍)

1. Spring WebFlux 2小时快速入门 Spring 5 之使用Spring WebFlux开发响应式应用. lambda与函数式(15min) Reactor 3 响应式编程库(60min) Spring Webflux和Spring Data Reactive开发响应式应用(45min) 通过以上内容相信可以对Spring 5.0 推出的响应式开发有了初步的体会.如果希望有更加深入的了解,欢迎阅读下边的系列文章-- 2. 响应式Spring的道法术器 这个系列的文章是为了记录下自

(10)响应式宣言、响应式系统与响应式编程——响应式Spring的道法术器

本系列文章索引<响应式Spring的道法术器>前情提要 响应式编程 | 响应式流 1.5 响应式系统 1.5.1 响应式宣言 关注"响应式"的朋友不难搜索到关于"响应式宣言"的介绍,先上图: 这张图凝聚了许多大神的智慧和经验,见官网,中文版官网,如果你认可这个宣言的内容,还可以签下你的大名.虽然这些内容多概念而少实战,让人感觉是看教科书,但是字字千金,不时看一看都会有新的体会和收获. 这也是新时代男朋友的行为准则: Responsive,要及时响应,24

附2:Reactor 3 之选择合适的操作符——响应式Spring的道法术器

本系列文章索引<响应式Spring的道法术器>前情提要 Reactor Operators 本节的内容来自我翻译的Reactor 3 参考文档--如何选择操作符.由于部分朋友打开github.io网速比较慢或上不去,贴出来方便大家查阅. 如果一个操作符是专属于 Flux 或 Mono 的,那么会给它注明前缀.公共的操作符没有前缀.如果一个具体的用例涉及多个操作符的组合,这里以方法调用的方式展现,会以一个点(.)开头,并将参数置于圆括号内,比如: .methodCall(parameter).

(1)什么是响应式编程——响应式Spring的道法术器

本系列文章索引:<响应式Spring的道法术器>. 1 响应式编程之道 1.1 什么是响应式编程? 在开始讨论响应式编程(Reactive Programming)之前,先来看一个我们经常使用的一款堪称"响应式典范"的强大的生产力工具--电子表格. 举个简单的例子,某电商网站正在搞促销活动,任何单品都可以参加"满199减40"的活动,而且"满500包邮".吃货小明有选择障碍(当然主要原因还是一个字:穷),他有个习惯,就是先在Excel

(2)响应式流——响应式Spring的道法术器

本系列文章索引:<响应式Spring的道法术器>.前情提要: 什么是响应式编程 1.2 响应式流 上一节留了一个坑--为啥不用Java Stream来进行数据流的操作? 原因在于,若将其用于响应式编程中,是有局限性的.比如如下两个需要面对的问题: Web 应用具有I/O密集的特点,I/O阻塞会带来比较大的性能损失或资源浪费,我们需要一种异步非阻塞的响应式的库,而Java Stream是一种同步API. 假设我们要搭建从数据层到前端的一个变化传递管道,可能会遇到数据层每秒上千次的数据更新,而显然

(12)Reactor 3 自定义数据流——响应式Spring的道法术器

本系列文章索引<响应式Spring的道法术器>前情提要 响应式流 | Reactor 3快速上手 | 响应式流规范本文源码 2.2 自定义数据流 这一小节介绍如何通过定义相应的事件(onNext.onError和onComplete) 创建一个 Flux 或 Mono.Reactor提供了generate.create.push和handle等方法,所有这些方法都使用 sink(池)来生成数据流. sink,顾名思义,就是池子,可以想象一下厨房水池的样子.如下图所示: 下面介绍到的方法都有一个

(12)自定义数据流(实战Docker事件推送的REST API)——响应式Spring的道法术器

本系列文章索引<响应式Spring的道法术器>前情提要 Reactor 3快速上手 | Spring WebFlux快速上手 | 响应式流规范本文 测试源码 | 实战源码 2.2 自定义数据流 这一小节介绍如何通过定义相应的事件(onNext.onError和onComplete) 创建一个 Flux 或 Mono.Reactor提供了generate.create.push和handle等方法,所有这些方法都使用 sink(池)来生成数据流. sink,顾名思义,就是池子,可以想象一下厨房水

(15)Reactor 3 Operators——响应式Spring的道法术器

本系列文章索引<响应式Spring的道法术器>前情提要 Reactor 3快速上手 | 响应式流规范 2.5 Reactor 3 Operators 虽然响应式流规范中对Operator(以下均称作"操作符")并未做要求,但是与RxJava等响应式开发库一样,Reactor也提供了非常丰富的操作符. 2.5.1 丰富的操作符 本系列前边的文章中,陆续介绍了一些常用的操作符.但那也只是冰山之一角,Reactor 3提供了丰富的操作符,如果要一个一个介绍,那篇幅大了去了,授人以

(14)Reactor调度器与线程模型——响应式Spring的道法术器

本系列文章索引<响应式Spring的道法术器>前情提要 Spring WebFlux快速上手 | Spring WebFlux性能测试前情提要:Reactor 3快速上手 | 响应式流规范 | 自定义数据流本文测试源码 2.4 调度器与线程模型 在1.3.2节简单介绍了不同类型的调度器Scheduler,以及如何使用publishOn和subscribeOn切换不同的线程执行环境. 下边使用一个简单的例子再回忆一下: @Test public void testScheduling() { F