要想更好的运用函数式编程,只是熟悉其语法结构是远远不够的。有必要从思想和规划层面,去考虑它,去接纳它。这种编程范式和大多数开发人员所熟知的面向方针编程范式是不同的。
下面咱们从以下几个方面来回想一下运用函数式编程的要害:
多用声明式,少用指令式
要想更好的运用函数式编程,首要有必要要进步代码的笼统程度。之所以运用函数式编程在完毕相同任务时需求的代码量比指令式要少,很大程度上便是源于函数式风格的代码的笼统程度更高,可以表达的语义也就更丰厚。
当咱们运用指令式风格的代码时,咱们习气很快地就进入到细节上的考量,比方处理这个问题需求运用多少次循环,每次循环需求进行的操作有哪些等。这是指令式风格的必定,正是因为较低的笼统程度而导致咱们的考虑办法也堕入了琐碎的细节中。此情此景,应了那句话“只见树木不见森林”。所以稍有经历的开发人员,都会特别垂青代码封装,意图也便是为了将笼统层次低的琐碎细节给躲藏起来,削减代码的噪声。
而运用函数式风格进行编码时,首要咱们应该明确这段代码的方针。比方,对集结根据某种条件进行过滤,对集结中的每个元素进行某种操作。这才是代码的方针,不要堕入到代码的底层细节中。比方,遍历集结并逐一判别,将符合要求的元素放入一个新的集结中,终究回来这个新的集结。一个用来简略判别你是否堕入到对细节的考虑的办法是:细心审视你的描绘中有没有出现“遍历”或许“循环”这样的字眼。假定出现了这些字眼,八成标明你的考虑办法过于底层了。在进行函数式编程时,这是一种需求防止的思想习气。所以,这或许便是之前提到过的内部遍历器(Internal Iterator)的意义,它将遍历的底层逻辑给封装起来,给你的思想腾出空间,让你有机会去从更微观,更笼统的视点来审视需求处理的问题。
比方,当咱们需求拿到一组价格中的最高价格时,99.9%的Java开发人员都会这样完毕:
int max = 0;
for(int price : prices) {
if(max < price) max = price;
}
以上便是最最典型的指令式考虑办法,严峻依赖于循环与遍历这类语法结构。但是细心想想,循环和遍历只不过是一种语法现象,它和咱们要处理的实际问题并没有直接的联系。咱们为何不从更高的视点来审视这个问题呢?输入的参数是一个集结,而输出的参数是一个元素。这便是典型的规约操作(Reduction Operation),它现已被JDK供应了,咱们只需求运用它即可:
final int max = prices . stream() . reduce(0, Math :: max);
代码变的简练了许多,没有额外创造出任何“轮子”,运用的都是现已存在的基础办法。比方reduce办法和Math.max的办法引证。这也是函数式风格的另一个吸引人的当地:可重用性。
喜爱不变性(Immutability)
关于并发程序规划而言,变量的可变功用够说是万恶之源。当多个线程一同对某个变量进行修改的时分,因为可能发生的竞态条件(Race Condition),在许多情况下会得到错误的效果。这也从另一个视点解说了为安在指令式的代码中,完毕高效而正确的并发代码是非常困难的。要处理的细节太多,各种小圈套和过于底层的完毕办法,能不困难吗?
而运用函数式编程时,就从本质上防止了创立过多的可变变量。程序的实施并不会改动方针的情况,而是经过输入的方针根据逻辑直接创立出了全新的方针。这也是为什么函数式代码更简单被并行化的原因,从根源上杜绝了损坏并行化的元凶巨恶 – 可变变量(Mutable Variable)。
因而,当你发现代码中定义了可变变量时,考虑是否有办法对它们进行重构来防止之。
削减副作用(Side Effects)
所谓副作用,便是这段代码对其外部的程序情况有影响。比方修改了某个实例的情况,修改了全局变量等等。
所谓的副作用,最常见的便是回来值为void的办法,这种办法一般都是经过修改方针的情况来完毕核算逻辑。这也意味着程序中存在着可变量。因而,办法的副作用往往和可变量有着千丝万缕的联络。当消除了办法的副作用时,八成也意味着可变量的数量也减小了。假定一个办法没有副作用,意味着只需输入的参数不变,输出的效果就永远是相同的。
当运用Lambda表达式时,需求确保代码没有副作用。假定代码存在副作用,也就违反了Lambda表达式的初衷,终究Lambda便是为了函数式编程而生的,而函数式编程的特征之一便是函数的完毕应该无副作用。
运用表达式(Expression),而不是句子(Statement)
表达式和句子都是程序中用来实施某些操作的指令。只不过它们之间存在一些差异:
- 句子:实施了操作,但是不会回来任何值
- 表达式:实施了操作,并且有回来值
因而,优先运用表达式也便是希望削减副作用和可变量。因为句子尽管没有回来值,但是它仍然会实施某些操作,而这些操作一般便是对程序情况进行修改,不然句子的意义安在?
一同,表达式和句子不相同也在于表达式是可以组合起来的,也便是前面提到过的函数链(Function Chaining)。在函数式编程中,函数链非常健壮,并且具有很好的可重用性,每个函数就像一块乐高积木,而这些积木则可以拼装出无穷无尽的组合。
规划高阶函数
高阶函数是Java 8中引进的一个严峻特性,以前咱们只能向办法传入方针或许值作为参数。而高阶函数的引进,让咱们也可以将函数作为参数传入。这无疑可以大大地进步代码的笼统程度,一同削减代码的噪声,比方以前频繁运用的匿名内部类,当它的定义符合函数接口的标准时,就可以直接运用Lambda表达式或许更为简练的办法引证。
比方,这样的代码在Java 8之前的GUI程序中层出不穷:
button . addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
JOptionPane . showMessageDialog(frame, "you clicked!");
}
});
而现在咱们可以让它插上Lambda表达式的翅膀:
button . addActionListener(event - >
JOptionPane . showMessageDialog(frame, "you clicked!"));
这不只让这部分代码变的简练,还削减了源代码中需求的import句子。 因为此刻需求完毕的接口ActionListener是不需求引进的,一同ActionEvent也可以经过凭仗类型推导来自动引进。
合理地规划和使用高阶函数,可以让咱们非常奇妙和简练地完毕一些常用的规划方式,而不需求像面向方针规划那样创立一大批的类型和接口。
关于功用
有些有些开发人员会忧虑,大规模的将指令式代码替换成以Lambda表达式和办法引证为基础的声明式代码会对程序功用构成一些影响。但是实际上,这个忧虑可以说是剩余的:在大多数情况下,功用只会变的更好。
Java 8标准供应了一些办法用来帮忙编译器进行优化。其间比较和Lambda表达式联系比较亲近的是一个叫做”调用动态优化的字节码“(Invoke Dynamic Optimized Bytecode)的指令。结合这个指令可以让Lambda的表达式的实施更快。
比方,下面是一段用来核算集结中质数个数的指令式程序:
long primesCount = 0;
for(long number : numbers) {
if(isPrime(number)) primesCount += 1;
}
将指令式重构成下面的声明式:
final long primesCount = numbers
.stream()
.filter(number - > isPrime(number))
.count();
当这个集结是1-100000的整型数时,无论是指令式仍是声明式的运行时间都是0.025秒左右。但是即便耗时相同,因为运用声明式的种种优势,咱们仍是可以以为声明式的风格更好:更简练,没有副作用,易于并行化。
并且,因为对每个整型数值判别其是否是质数都是独立的任务。因而,上述程序也非常简单被并行化,只是是将stream办法替换成parallelStream就行:
final long primesCount = numbers
.parallelStream()
.filter(number - > isPrime(number))
.count();
此刻,实施时间缩短到了0.006秒!也便是说,功用上升了大约400%。
选用函数式编码风格
从Java 8开端,Java也是一门像Scala等言语那样的混合范式编程言语了。了解在Java 8中怎样经过Lambda表达式,办法引证和高阶函数等语法来完毕函数式编码自身并不困难。
困难的当地在于,怎样灵敏地将这种新式编程范式和现有的指令式面向方针范式有机地融合在一同。这需求你对手头的问题进行细心的考虑,乃至是颠覆性的考虑。当然,经过寻觅一些典型的用例也是一个非常好的学习办法。
咱们可以采纳一种故步自封的办法来对编写代码。首要让它可以作业,再让它变的更好,变的更美丽。而毫无疑问,函数式编码会让代码愈加美丽,愈加充溢诗意。