随便聊聊 Java 8 的函数式编程
函数式编程(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.Runnable
和 java.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 对象,而终止操作返回的是具体的结果(或者说归约,按照给定的规则对流中的元素进行加工,然后输出到结果中)
- 常见的中间操作:
map
、filter
、limit
- 常见的终止操作:
max
、collect
、forEach
- 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
顾名思义,就是收集器,就是按照一定的规则,对流进行数据的归约操作。
刚刚 map
、filter
的示例中都用到了 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 已经给我们提供了不少常用的收集器了,用于实现像 join
、groupingBy
等常用操作,可以直接看一下 Collectors
的类注释,官方很贴心的给我们提供了不少示例代码。
好了,就写这么多吧,希望对大家有所帮助。
随便聊聊 Java 8 的函数式编程的更多相关文章
- 浅谈Java 8的函数式编程
函数式编程语言是什么? 函数式编程语言的核心是它以处理数据的方式处理代码.这意味着函数应该是第一等级(First-class)的值,并且能够被赋值给变量,传递给函数等等.(转载自http://xz.p ...
- Java 中的函数式编程(Functional Programming):Lambda 初识
Java 8 发布带来的一个主要特性就是对函数式编程的支持. 而 Lambda 表达式就是一个新的并且很重要的一个概念. 它提供了一个简单并且很简洁的编码方式. 首先从几个简单的 Lambda 表达式 ...
- Scala:用于Java的轻量级函数式编程
Scala为Java开发提供了轻量级的代码选项,但是学习过程可能会很艰难.了解有关Scala的知识以及是否值得采用. 基于Java的语言通常涉及冗长的语法和特定于领域的语言,用于测试,解析和数值计算过 ...
- Java中的函数式编程(二)函数式接口Functional Interface
写在前面 前面说过,判断一门语言是否支持函数式编程,一个重要的判断标准就是:它是否将函数看做是"第一等公民(first-class citizens)".函数是"第一等公 ...
- Java中的函数式编程(三)lambda表达式
写在前面 lambda表达式是一个匿名函数.在Java 8中,它和函数式接口一起,共同构建了函数式编程的框架. lambda表达式乍看像是匿名内部类的一种语法糖,但实际上,它们是两种本质不同的事物 ...
- Java中的函数式编程(五)Java集合框架中的高阶函数
写在前面 随着Java 8引入了函数式接口和lambda表达式,Java 8中的集合框架(Java Collections Framework, JCF)也增加相应的接口以适应函数式编程. 本文的 ...
- Java中的函数式编程(六)流Stream基础
写在前面 如果说函数式接口和lambda表达式是Java中函数式编程的基石,那么stream就是在基石上的最富丽堂皇的大厦. 只有熟悉了stream,你才能说熟悉了Java 的函数式编程. 本文主要介 ...
- Java中的函数式编程(七)流Stream的Map-Reduce操作
写在前面 Stream 的 Map-Reduce 操作是Java 函数式编程的精华所在,同时也是最为复杂的部分.但一旦你啃下了这块硬骨头,那你就真正熟悉Java的函数式编程了. 如果你有大数据的编程经 ...
- Java中的函数式编程(八)流Stream并行编程
写在前面 在本系列文章的第一篇,我们提到了函数式编程的优点之一是"易于并发编程". Java作为一个多线程的语言,它通过 Stream 来提供了并发编程的便利性. 题外话: 严格来 ...
随机推荐
- nasm astrlen函数 x86
xxx.asm %define p1 ebp+8 %define p2 ebp+12 %define p3 ebp+16 section .text global dllmain export ast ...
- Flutter: moor_flutter库,简化sqlite操作
入门 video moor_flutter 示例代码仓库 install dependencies: ... moor_flutter: dev_dependencies: ... moor_gene ...
- Gradle 差异化构建
Compile 默认的依赖方式,任何情况下都会依赖. Provided 只提供编译时依赖,打包时不会添加进去. Apk 只在打包Apk包时依赖,这个应该是比较少用到的. TestCompile 只在测 ...
- QT现场同步
// 1线程同步 QFutureSynchronizer<void> synchronizer; //2线程1 synchronizer.addFuture(QtConcurrent::r ...
- this指针、引用、顶层和底层const关系
1.首先顶层const和底层const是围绕指针*p的说法.底层:const int *p,const不是修饰指针p,指针所指的值不能改变:顶层:int *const p,const修饰指针p,指针本 ...
- 《C++ Primer》笔记 第3章 字符串、向量和数组
位于头文件的代码一般来说不应该使用using声明. 如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去.与之相反,如果不使用等号,则执行的是直接 ...
- scrapy框架的介绍与安装
scrapy框架的原理 使用pycharm安装scrapy库 1.打开新建file,然后有个扳手的setings点击进去,如图所示: 2.选择project 然后点击python interprete ...
- WPF 基础 - 图片与 base64
1. base64 转图片 将 base64 转成 byte[] 将 byte[] 作为内存流保存到一个 BitmapImage 实例的流的源 把 BitmapImage 作为目标图片的 Source ...
- Kubernetes 实战 —— 03. pod: 运行于 Kubernetes 中的容器
介绍 pod P53 pod 是 Kubernetes 中最为重要的核心概念,而其他对象仅仅用于 pod 管理. pod 暴露或被 pod 使用. pod 是一组并置的容器,代表了 Kubernete ...
- golang io操作之写篇
/** * @author livalon * @data 2018/9/4 15:11 */ package main import ( "os" "fmt" ...