Java8 的函数式编程
什么是函数式编程
简单说,「函数式编程」是一种「编程范式」,也就是如何编写程序的方法论。
它属于「结构化编程」结构化编程的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。举例来说,现在有这样一个数学表达式:
(1 + 2) * 3 - 4
传统的过程式编程,可能这样写:
var a = 1 + 2;
var b = a * 3;
var c = b - 4;
函数式编程要求使用函数,我们可以把运算过程定义为不同的函数,然后写成下面这样:
var result = subtract(multiply(add(1,2), 3), 4);
这就是函数式编程。
编程范式
程序编程范式。一个是左脑,一个右脑。我们程序员基本上是在用左脑,左脑是理性分析,喜欢数据证据,线性思维,陷入细节,具体化的,不抽象。但是实际上玩儿出这些东西的都在右脑,函数式,还有像逻辑式的抽象能力都在右脑。所以我们非线性的想象力都在这边,而标准化教育把我们这边已经全部干掉了,我们只剩左边。
函数式编程或是声明式编程,需要的是用我们的右脑,而指令式的则需要用我们的左脑。很多人理解和使用声明式的编程范式比较有困难,因为这要用你的右脑,但是我们习惯于用我们的左脑,左脑用多了以后右脑就有点跟不上了。
函数式编程的特性
函数是第一等公民
所谓「第一等公民」,指的是函数与其他数据类型一样,处于平等地位,它不仅拥有一切传统函数的使用方式(声明和调用),可以赋值给其他变量(赋值),也可以作为参数,传入另一个函数(传参),或者作为别的函数的返回值(返回)。函数可以作为参数进行传递,意味我们可以把行为「参数化」,处理逻辑可以从外部传入,这样程序就可以设计得更灵活
|
|
|
|
没有副作用
指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。函数式编程强调没有「副作用」,意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。
引用透明
指的是函数的运行不依赖于外部变量或「状态」,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。这里强调了一点:输入不变则输出也不变,就像数学函数里面的f(x),只要输入的x一样那得到的结果也肯定定是一样的。
函数式编程的意义
代码简洁,开发快速。
函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。Paul Graham在《黑客与画家》一书中写道:同样功能的程序,极端情况下,Lisp代码的长度可能是C代码的二十分之一。如果程序员每天所写的代码行数基本相同,这就意味着,“C语言需要一年时间完成开发某个功能,Lisp语言只需要不到三星期。反过来说,如果某个新功能,Lisp语言完成开发需要三个月,C语言需要写五年。“当然,这样的对比故意夸大了差异,但是"在一个高度竞争的市场中,即使开发速度只相差两三倍,也足以使得你永远处在落后的位置。”
接近自然语言,易于理解
函数式编程的自由度很高,可以写出很接近自然语言的代码。以 Java 为例把学生以性别分组:
没用 Lambda 表达式
|
|
用了 Lambda 表达式:
|
|
这基本就是自然语言的表达了,大家应该一眼就能明白它的意思吧。
更方便的代码管理
函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试和 Debug,以及模块化组合。
易于并发编程
函数式编程不需要考虑死锁,因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署并发编程。
请看下面的代码:
|
|
由于 s1 和 s2 互不干扰,不会修改变量,谁先执行是无所谓的,所以可以放心地增加线程,把它们分配在两个线程上完成。其他类型的语言就做不到这一点,因为 s1 可能会修改系统状态,而 s2 可能会用到这些状态,所以必须保证 s2 在 s1 之后运行,自然也就不能部署到其他线程上了。多核 CPU 是将来的潮流,所以函数式编程的这个特性非常重要。
Java8 的 Lambda 表达式
Java8 中 Lambda 表达式由三个部分组成:第一部分为一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数;第二部分为一个箭头符号:->;第三部分为方法体,可以是表达式和代码块。语法如下 方法体为表达式,该表达式的值作为返回值返回。
|
|
方法体为代码块,必须用 {} 来包裹起来,且需要一个 return 返回值,但若函数式接口里面方法返回值是 void,则无需返回值。
|
|
能够接收 Lambda 表达式的参数类型,是一个只包含一个方法的接口。只包含一个方法的接口称之为函数接口,通常用@FunctionalInterface
注解修饰。
|
|
|
|
由此可以看出 Lambda 表达式的主要用途就是替换匿名类,而实现 Runnable 接口是匿名类的最好示例。看一下 Java8 之前的 Runnable 实现方法,需要 4 行代码,而使用 Lambda 表达式只需要一行代码。我们在这里做了什么呢?那就是用() -> {}
代码块替代了整个匿名类。
|
|
最佳实践
保持Lambda表达式简短和一目了然
|
|
长长的Lambda表达式通常是危险的,因为代码越长越难以读懂,意图看起来也不明,并且代码也难以复用,测试难度也大。
使用@FunctionalInterface 注解
如果你确定了某个 interface 是用于 Lambda 表达式,请一定要加上@FunctionalInterface
,表明你的意图。不然将来说不定某个不知情的家伙比如你旁边的好基友,在这个 interface 上面加了另外一个抽像方法时,你的代码就悲剧了。
优先使用java.util.function包下面的函数式接口
java.util.function
这个包下面提供了大量的功能性接口,可以满足大多数开发人员为 Lambda 表达式和方法引用提供目标类型的需求。每个接口都是通用的和抽象的,使它们易于适应几乎任何 Lambda 表达式。开发人员应该在创建新的功能接口之前研究这个包,避免重复定义接口。另外一点就是,里面的接口不会被别人修改。
不要在Lambda表达中执行有副作用的操作
副作用是严重违背函数式编程的设计原则,在工作中我经常看到有人在 forEach 操作里面操作外面的某个 List 或者设置某个 Map 这其实是不对的。
不要把 Lambda 表达式和匿名内部类同等对待
虽然我们可以用匿名内部类来实现 Lambda 表达式,也可以用 Lambda 表达式来替换内部类,但并不代表这两者是等价的。这两者在某一个重要概念是不同的:this
指代的上下文是不一样的。当您使用内部类时,它将创建一个新的范围。通过实例化具有相同名称的新局部变量,可以从封闭范围覆盖局部变量。您还可以在内部类中使用这个关键字作为它实例的引用。但是,Lambda表达式可以使用封闭范围。您不能在 Lambda的主体内覆盖范围内的变量
|
|
运行上面这段代码我们将到resultIC = "Inner class value",resultLambda = "Enclosing scope value"
。也就是说在匿名内部类中this
指的是自身的引用,在Lambda 表达式中this
指的是外部。
多使用方法引用
在 Lambda 表达式中a -> a.toLowerCase()
和String::toLowerCase
都能起到相同的作用,但两者相比,后者通常可读性更高并且代码会简短。
尽量避免在 Lambda 的方法体中使用 {} 代码块
优先使用
|
|
而不是
|
|
- 原文作者:范明勇
- 原文链接:https://blog.fanmuyong.com/post/Java8-%E7%9A%84%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。