函数式编程(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
的类注释,官方很贴心的给我们提供了不少示例代码。
好了,就写这么多吧,希望对大家有所帮助。
原文转载:http://www.shaoqun.com/a/684743.html
tradeindia:https://www.ikjzd.com/w/2305
淘粉吧返利:https://www.ikjzd.com/w/1725
函数式编程(FunctionalProgramming)首先,我们来了解一个叫做"编程范式"的概念。什么是"编程范式"呢?简单来说就是指导我们编程的方法论,是一种教我们如何编写代码、如何组织代码的"解题"思路。业界普遍将编程范式划分为两大类:指令式和过程式,当然每个大类也可以接着往下细分,下面只列出了一些比较常见的子分类:指令式(Impe
万国邮政联盟:https://www.ikjzd.com/w/861
eprice:https://www.ikjzd.com/w/1325
review:https://www.ikjzd.com/w/2735
我和老公两地分居了近8年:http://www.30bags.com/a/253194.html
Upfluence:https://www.ikjzd.com/w/2020
聊聊锦上添花的亚马逊新品期!!!:https://www.ikjzd.com/home/133352
没有评论:
发表评论