函数式编程(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. nasm astrlen函数 x86

    xxx.asm %define p1 ebp+8 %define p2 ebp+12 %define p3 ebp+16 section .text global dllmain export ast ...

  2. Flutter: moor_flutter库,简化sqlite操作

    入门 video moor_flutter 示例代码仓库 install dependencies: ... moor_flutter: dev_dependencies: ... moor_gene ...

  3. Gradle 差异化构建

    Compile 默认的依赖方式,任何情况下都会依赖. Provided 只提供编译时依赖,打包时不会添加进去. Apk 只在打包Apk包时依赖,这个应该是比较少用到的. TestCompile 只在测 ...

  4. QT现场同步

    // 1线程同步 QFutureSynchronizer<void> synchronizer; //2线程1 synchronizer.addFuture(QtConcurrent::r ...

  5. this指针、引用、顶层和底层const关系

    1.首先顶层const和底层const是围绕指针*p的说法.底层:const int *p,const不是修饰指针p,指针所指的值不能改变:顶层:int *const p,const修饰指针p,指针本 ...

  6. 《C++ Primer》笔记 第3章 字符串、向量和数组

    位于头文件的代码一般来说不应该使用using声明. 如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去.与之相反,如果不使用等号,则执行的是直接 ...

  7. scrapy框架的介绍与安装

    scrapy框架的原理 使用pycharm安装scrapy库 1.打开新建file,然后有个扳手的setings点击进去,如图所示: 2.选择project 然后点击python interprete ...

  8. WPF 基础 - 图片与 base64

    1. base64 转图片 将 base64 转成 byte[] 将 byte[] 作为内存流保存到一个 BitmapImage 实例的流的源 把 BitmapImage 作为目标图片的 Source ...

  9. Kubernetes 实战 —— 03. pod: 运行于 Kubernetes 中的容器

    介绍 pod P53 pod 是 Kubernetes 中最为重要的核心概念,而其他对象仅仅用于 pod 管理. pod 暴露或被 pod 使用. pod 是一组并置的容器,代表了 Kubernete ...

  10. golang io操作之写篇

    /** * @author livalon * @data 2018/9/4 15:11 */ package main import ( "os" "fmt" ...