函数式编程(Functional Programming)

首先,我们来了解一个叫做"编程范式"的概念。

什么是"编程范式"呢?简单来说就是指导我们编程的方法论,是一种教我们如何编写代码、如何组织代码的"解题"思路。

业界普遍将编程范式划分为两大类:指令式过程式,当然每个大类也可以接着往下细分,下面只列出了一些比较常见的子分类:

  • 指令式(Imperative)

    • 过程式(Procedural),比如:Fortran、C
    • 面向对象(Object Oriented),比如:C++、Java
  • 声明式(Declarative)
    • 逻辑式(Logic),比如:Prolog
    • 函数式(Functional),比如:Haskell、Erlang

在上面的分类中,有的同学看到 Java 被划分到了指令式范式的范畴可能会感觉到疑惑,其实指令式范式与声明式范式之间并没有绝对的界限,而且大有相互渗透的趋势,上述的分类也只是一个广义的划分,不用太过较真,稍微有个概念就行了。

指令式范式更关注过程,侧重于描述"如何去做",而声明式范式关注结果,侧重于描述"去做什么"

来看一个 Java 计算阶乘的例子:

// 给出明确的算法实现(指令式)
public int factorialImperative(int n) {
int f = 1;
for (; n > 0; --n) {
f *= n;
}
return f;
}
// 仅仅给出了阶乘的递归定义(声明式)
public int factorialDeclarative(int n) {
return n == 0 ? 1 : n * factorialDeclarative(n - 1);
}

什么是函数式编程

关于什么是函数式编程貌似也没有什么官方的定义,我好像也没有找到什么权威的解释,参考 wikipedia 以及自己的一些理解大概描述一下吧:

函数式编程是一种声明式的编程范式(以目标为导向,侧重于描述"去做什么"),通过函数运算以及函数组合的方式构建程序,并且避免使用共享状态(变量)以及可变对象。

好吧,总结的好像也比较抽象。没关系,还是有个概念就好,多写写多看看就有感觉了。

说说函数式编程中一些常见的概念吧:

  • First-class function(一言以蔽之:函数可以赋值给变量)
  • High-order function(一言以蔽之:函数可以作为另一个函数的入参或者出参)
  • Pure function(一言以蔽之:给定输入,输出总是固定的)
    • 补充说明,正是因为输出总是依赖输入,所以引申出一些其他的概念

      • Side Effect Free(没有副作用的,即不会污染上下文)
      • Reference Transparency(引用透明,即不依赖外部变量)
      • Lazy Evaluation(延迟求值,即调用的时机就不那么重要了)
  • 不可变(这个很好理解,当一切都是不可变,也就不需要考虑锁竞争了,也不用担心并发问题了,并行也变得简单起来)
  • 科里化(一种模块化和重用代码的技术,说白了其实就是闭包的一种实践)
  • 延迟列表、模式匹配还有结合器等等大家感兴趣的话可以自己去谷歌一下

Java 8 的函数式编程

Java 8 对函数式编程的支持

众所周知 Java 属于面向对象编程语言,广义上属于指令式范式的语言,但是没必要较真这个范式的分类,或是追求写出标准的函数式编程风格的代码,最重要的还是写出好的代码,业务价值OK、扩展性OK、维护性OK(当然好代码可不止这些标准)。

下面我们就来看看 Java 为了支持函数式编程,做了哪些努力。

首先,就是参数传递。Java 为了实现把函数当做参数来传递,引入了一个函数式接口(Functional Inteface)的概念。

函数式接口

简单来说函数式接口就是只定义一个方法的接口。比如我们经常会用到的 java.lang.Runnablejava.util.concurrent.Callable

但是这样的定义未免有点单薄,因为只要人为的在接口里多添加一个方法,那么这个接口就不是函数接口了,又或者说某个接口压根儿就不是函数式接口,但是这个接口也只定义了一个方法,恰巧也符合函数式接口的语义,比如 java.lang.AutoCloseable,这就会给我们使用带来很多歧义和误导。

Java 8 提供了 @FunctionalInterface 注解来解决上面提到的问题。跟泛型和 @Override 类似,@FunctionalInterface 也同样是编译器来给代码做约束的,提供安全性的同时也提高了代码的可读性(因为显示的给你标记了嘛)。

当然,这个注解也不是必须的,还是举 java.lang.AutoCloseable 这个例子,像下面的声明编译器也并不会报错,但是这并没有什么实际意义,这些都是代码里面的坏味道,切记不可这样写:

public static void main(String[] args) throws Exception {
AutoCloseable unscientific = () -> {
System.out.println("不科学的示例...");
};
unscientific.close();
}

在上面的示例代码中,引入了 Java 8 的一个新的语法特性,你应该注意到了,就是 () -> {}:一个参数块+一个箭头+一个代码块,这就是 Lamdba 表达式。

Lambda 表达式

Java 8 引入 Lambda 表达式的主要作用就是简化代码编写,它只是一个语法糖而已,底层还是基于函数接口来实现的,如下面的示例那样:

// 基于函数接口
Runnable task1 = new Runnable() {
@Override
public void run() {
// business code
}
};
// Lamdba 写法
Runnable task2 = () -> {
// business code
};

以前,我们只能通过匿名内部类将代码作为数据(也可以说是行为)来传递,写过匿名内部类的小伙伴都知道,样板代码你肯定是躲不开的,可读性也没那么友好,而现在,Lambda 表达式给我们提供了一种更加紧凑的传递行为的方式。

可以把 Lambda 表达式理解为函数式接口的一个具体实现的实例。Java 8 也允许我们直接以内联的形式直接编写函数式接口的抽象实现,而且,还可以把整个表达式直接当成参数进行传递。

最简单的 Lambda 表达式可以用逗号分隔的参数列表、-> 箭头符号以及语句块:

Arrays.asList("a", "b", "d")
.forEach(e -> System.out.println(e));

Lambda表达式可能会有返回值,编译器会根据上下文推断返回值的类型,如果lambda的语句块只有一行,可以省略return关键字。

// 传统的方式
Arrays.asList("a", "d", "c").sort(new Comparator<String>() {
@Override
public int compare(String e1, String e2) {
return e1.compareTo(e2);
}
});
// Lambda 语法
Arrays.asList("a", "d", "c").sort((e1, e2) -> {
int result = e1.compareTo(e2);
return result;
});
// Lambda 语法精简
Arrays.asList("a", "d", "c").sort((e1, e2) -> e1.compareTo(e2));

关于 Lamdba 再说两点:

  • 对局部变量的限制

    我们在使用过程中,常常发现 Lamdba 引用局部变量必须是final的或者说是隐式的final的,这主要是因为实例变量和局部变量背后的实现有一个关键不同:实例变量都存储在堆中,而局部变量则保存在栈上。堆是线程共享的,而栈是线程私有的,换句话说,Lambda 表达式引用的是值,而不是变量。
  • 方法引用

    Java 8 还提供了方法应用来进一步简化代码的编写,就像上个示例中代码,我们还能进一步简写为:
    // 方法引用
    Arrays.asList("a", "b", "d").sort(String::compareTo);

好了,刚刚介绍了 Java 8 的函数式接口还有 Lambda 表达式,可以进入下一个阶段了,就是流(Stream)。

流(Stream)

举个电商计算 ROI(投资回报率)的简单例子,公式如下:

ROI =[(收入-成本)/ 投入 ]*100

常规操作可能写成这样:

multiply(divide(subtract(收入,成本),投入),100)

但如果我们换一种可读性更高的写法:

subtract(收入,成本).divide(投入).multiply(100)

是不是立马眼前一亮!我们知道 . 操作符是 Java 里面的方法调用,为了支持上面的这种级联操作,Java 8 让每个方法都返回一个通用的类型,即:Stream。

流有两个特点

  • 流水线(简单来说就是函数的级联调用,可以类比SQL语句)
  • 内部迭代(简单来说就是不需要像集合那样显示的处理迭代)

"流水线"这个特点就像刚刚计算ROI公式的那个例子,比较明了,我们来看看内部迭代:

在 Java 8 以前,我们大量的使用集合,对于稍微复杂一些的操作(过滤、分组、排序等组合操作)我们通常需要显示的编写大量的代码,当然,这也是无法避免的,这又回到文章开头提到的指令式范式的特点上面了,在没有声明式编程语法的支持下,你只能给出具体的算法实现,这个就不举例子了,大家应该深有体会,还是举个简单的例子吧:

int count=0;
for (Person person : persons) {
if (person.isFrom("江苏"))){
count++;
}
}

现在 Java 8 引入了流,在流上支持了声明式的编程风格,一切就都变得豁然开朗起来,语义也更加丰满起来了:

long count = persons.stream()
.filter(person -> person.isFrom("江苏"))
.count()

流的惰性求值

  • Stream 上的有两种操作:中间操作和终止操作

    • 中间操作仍然返回 Stream 对象,而终止操作返回的是具体的结果(或者说归约,按照给定的规则对流中的元素进行加工,然后输出到结果中)
    • 常见的中间操作:mapfilterlimit
    • 常见的终止操作:maxcollectforEach
  • Stream 补充说明
    • 流的目的在于表达计算,集合讲的是数据,而流讲的是计算。粗略地说,集合与流之间的差异就在于什么时候进行计算。流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值
    • 从有序集合生成流时会保留原有的顺序,由列表生成的流其元素顺序与列表一致
    • 流操作可以顺序执行,也可以并行执行
    • 注意,和迭代器类似,流只能消费一次

常用的流操作

map

如果有一个函数可以将一种类型的值转换成另外一种类型,map 操作就可以使用该函数,将一个流中的值转换成一个新的流。

List<String> results = Stream.of("a", "c", "c")
.map(String::toUpperCase)
.collect(Collectors.toList());
// output: [A, B, C]
filter

遍历数据并检查其中的元素时,可尝试使用 Stream 中提供的新方法 filter

List<Integer> results = Stream.of("1", "2", "3")
.map(Integer::parseInt)
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// output: [2]
flatMap

flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream

List<Integer> results = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4))
.flatMap(List::stream)
.collect(Collectors.toList());
// output: [1, 2, 3, 4]
reduce

reduce 操作可以实现从一组值中生成一个值。

int result1 = IntStream.of(1, 2, 3, 4)
.sum(); int result2 = Stream.of(1, 2, 3, 4)
.reduce(0, (acc, element) -> acc + element); int result3 = Stream.of(1, 2, 3, 4)
.reduce(0, Integer::sum); // output: 10

Collector

最后,我们来看一看 Collector接口。Stream 接口中定义了一个 <R, A> R collect(Collector<? super T, A, R> collector); 的方法,接收一个 Collector 类型的参数。Collector 顾名思义,就是收集器,就是按照一定的规则,对流进行数据的归约操作。

刚刚 mapfilter 的示例中都用到了 Stream.collect(Collectors.toList()); :将流中的元素输出到一个 List 中去。

我们先看一下 Collector 的接口定义:

// 建立新的结果容器
Supplier<A> supplier();
// 将元素添加到结果容器
BiConsumer<A, T> accumulator();
// 合并两个结果容器
BinaryOperator<A> combiner();
// 对结果容器应用最终转换
Function<A, R> finisher();
// 定义了收集器的行为(尤其是关于流是否可以并行归约),以及可以使用哪些优化的提示。
Set<Characteristics> characteristics();

如果我们想把流中的元素归约到一个 List<Integer> 中,那么首先,你得有一个收集结果的容器:

Supplier<List<Integer>> containerSupplier = ArrayList::new;

然后我们需要一个累加函数,将流中的一个个元素放到结果容器中:

BiConsumer<List<Integer>, Integer> accumulator = List::add;

然后,我们需要告诉编译器这个流是否可以并行归约,或是可以做哪些优化:

Set<Collector.Characteristics> characteristics = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));

然后如果是在并行的场景下,我们需要将并行的各个部分进行合并,这个时候,就需要一个合并的函数:

BinaryOperator<List<Integer>> combiner = (List<Integer> left, List<Integer> right) -> {
left.addAll(right);
return left;
};

最后,我们需要一个类型转换的函数,将流中的元素转换成归约的目标类型:

Function<Integer, Integer> finisher = (Integer i) -> (Integer) i;

所以整体思路就是动态新建了一个目标集合,然后遍历这个流,将流中的元素依次添加到目标集合中,如果不考虑并行的情况,其实Collector就是干了这么一件事。如果你要实现自己的收集器,大体上,也就是跟这5个方法打交道了。

当然,Java 已经给我们提供了不少常用的收集器了,用于实现像 joingroupingBy 等常用操作,可以直接看一下 Collectors 的类注释,官方很贴心的给我们提供了不少示例代码。

好了,就写这么多吧,希望对大家有所帮助。

随便聊聊 Java 8 的函数式编程的更多相关文章

  1. 浅谈Java 8的函数式编程

    函数式编程语言是什么? 函数式编程语言的核心是它以处理数据的方式处理代码.这意味着函数应该是第一等级(First-class)的值,并且能够被赋值给变量,传递给函数等等.(转载自http://xz.p ...

  2. Java 中的函数式编程(Functional Programming):Lambda 初识

    Java 8 发布带来的一个主要特性就是对函数式编程的支持. 而 Lambda 表达式就是一个新的并且很重要的一个概念. 它提供了一个简单并且很简洁的编码方式. 首先从几个简单的 Lambda 表达式 ...

  3. Scala:用于Java的轻量级函数式编程

    Scala为Java开发提供了轻量级的代码选项,但是学习过程可能会很艰难.了解有关Scala的知识以及是否值得采用. 基于Java的语言通常涉及冗长的语法和特定于领域的语言,用于测试,解析和数值计算过 ...

  4. Java中的函数式编程(二)函数式接口Functional Interface

    写在前面 前面说过,判断一门语言是否支持函数式编程,一个重要的判断标准就是:它是否将函数看做是"第一等公民(first-class citizens)".函数是"第一等公 ...

  5. Java中的函数式编程(三)lambda表达式

    写在前面 lambda表达式是一个匿名函数.在Java 8中,它和函数式接口一起,共同构建了函数式编程的框架.   lambda表达式乍看像是匿名内部类的一种语法糖,但实际上,它们是两种本质不同的事物 ...

  6. Java中的函数式编程(五)Java集合框架中的高阶函数

    写在前面 随着Java 8引入了函数式接口和lambda表达式,Java 8中的集合框架(Java Collections Framework, JCF)也增加相应的接口以适应函数式编程.   本文的 ...

  7. Java中的函数式编程(六)流Stream基础

    写在前面 如果说函数式接口和lambda表达式是Java中函数式编程的基石,那么stream就是在基石上的最富丽堂皇的大厦. 只有熟悉了stream,你才能说熟悉了Java 的函数式编程. 本文主要介 ...

  8. Java中的函数式编程(七)流Stream的Map-Reduce操作

    写在前面 Stream 的 Map-Reduce 操作是Java 函数式编程的精华所在,同时也是最为复杂的部分.但一旦你啃下了这块硬骨头,那你就真正熟悉Java的函数式编程了. 如果你有大数据的编程经 ...

  9. Java中的函数式编程(八)流Stream并行编程

    写在前面 在本系列文章的第一篇,我们提到了函数式编程的优点之一是"易于并发编程". Java作为一个多线程的语言,它通过 Stream 来提供了并发编程的便利性. 题外话: 严格来 ...

随机推荐

  1. flutter 插件调用callback函数

    dart plugin class TestLib { static MethodChannel _channel = const MethodChannel('test_lib') ..setMet ...

  2. Flutter使用WebSockets

    文档 注意是WebSockets而不是socket.io install dependencies: web_socket_channel: demo import 'dart:convert'; i ...

  3. 5分钟入门websocket

    5 个步骤快速掌握消息发送和接收 获取您的 appkey 先注册一个irealtime账号,然后登录到后台管理端,创建一个免费应用,就能得到您的 appkey.点击注册 各种前端生态端集成 ireal ...

  4. [转]ubuntu系统重新分区、根目录扩容

    原文地址:https://blog.csdn.net/code_segment/article/details/79237500,转载主要方便随时查阅,如有版权要求,请及时联系. gparted是一款 ...

  5. Python数据结构与算法_最长公共前缀(05)

    编写一个函数来查找字符串数组中的最长公共前缀. 如果不存在公共前缀,返回空字符串 "". 示例 1: 输入: ["flower","flow" ...

  6. flex图片垂直居中

    html <view class="person_info_more"> <image class="more" src="/ima ...

  7. 1079 Total Sales of Supply Chain ——PAT甲级真题

    1079 Total Sales of Supply Chain A supply chain is a network of retailers(零售商), distributors(经销商), a ...

  8. vue学习遇到的问题

    1.vue脚手架的安装,解决链接:https://www.cnblogs.com/qcq0703/p/14439467.html2.2.2.0+ 的版本里,当在组件上使用 v-for 时,key 现在 ...

  9. [计算机图形学]视图变换:MVP变换、视口变换

    目录 一.MVP变换 1. 模型变换 1.1 缩放矩阵 1.2 旋转矩阵 1.3 平移矩阵 2. 视角变换 3. 投影变换 二.Viewport变换 一.MVP变换 MVP变换是模型变换(M).视角变 ...

  10. 使用gitlab构建基于docker的持续集成(一)

    使用gitlab构建基于docker的持续集成(一) gitlab docker aspnetcore 持续集成 开篇 整体环境规划 准备工作 CA证书 虚拟机系统:安装Centos7.3 3.设置C ...