本系列文章译自Venkat Subramaniam的Functional Programming in Java
第四节:进化而非革命
我们用不着转向别的语言,就能享受函数式编程带来的好处;需要改变的只是使用Java的一些方式。C++,Java,C#这些语言都支持命令式和面向对象的编程。不过现在它们都开始投入函数式编程的怀抱里了。我们刚才已经看到了这两种风格的代码,并讨论了函数式编程能带来的好处。现在我们来看下它的一些关键概念和例子来帮助我们学习这种新的风格。
Java语言的开发团队花费了大量的时间和精力把函数式编程的能力添加到了Java语言和JDK里。要享受它带来的好处,我们得先介绍几个新的概念。我们只要遵循下面几条规则就能提升我们的代码质量:
- 声明式
- 提倡不可变性
- 避免副作用
- 优先使用表达式而不是语句
- 使用高阶函数进行设计
我们来看下这几条实践准则。
声明式
我们所熟悉的命令式编程的核心就是可变性和命令驱动的编程。我们创建变量,然后不断修改它们的值。我们还提供了要执行的详细的指令,比如生成迭代的索引标志,增加它的值,检查循环是否结束,更新数组的第N个元素等。在过去由于工具的特性和硬件的限制,我们只能这么写代码。
我们也看到了,在一个不可变集合上,声明式的contains方法比命令式的更容易使用。所有的难题和低级的操作都在库函数里来实现了,我们不用再关心这些细节。就冲着简单这点,我们也应该使用声明式编程。不可变性和声明式编程是函数式编程的精髓,现在Java终于把它变成了现实。
提倡不可变性
变量可变的代码会有很多活动路径。改的东西越多,越容易破坏原有的结构,并引入更多的错误。有多个变量被修改的代码难于理解也很难进行并行化。不可变性从根本上消除了这些烦恼。
Java支持不可变性但没有强制要求——但我们可以。我们需要改变修改对象状态这个旧习惯。我们要尽可能的使用不可变的对象。
声明变量,成员和参数的时候,尽量声明为final的,就像Joshua Bloch在” Effective Java“里说的那句名言那样,“把对象当成不可变的吧”。
当创建对象的时候,尽量创建不可变的对象,比如String这样的。创建集合的时候,也尽量创建不可变或者无法修改的集合,比如用Arrays.asList()和Collections的unmodifiableList()这样的方法。
避免了可变性我们才可以写出纯粹的函数——也就是,没有副作用的函数。
避免副作用
假设你在写一段代码到网上去抓取一支股票的价格然后写到一个共享变量里。如果我们有很多价格要抓取,我们得串行的执行这些费时的操作。如果我们想借助多线程的能力,我们得处理线程和同步带来的麻烦事,防止出现竞争条件。最后的结果是程序的性能很差,为了维护线程而废寝忘食。如果消除了副作用,我们完全可以避免这些问题。
没有副作用的函数推崇的是不可变性,在它的作用域内不会修改任何输入或者别的东西。这种函数可读性强,错误少,容易优化。由于没有副作用,也不用再担心什么竞争条件或者并发修改了。不仅如此,我们还可以很容易并行执行这些函数,我们将在145页的来讨论这个。
优先使用表达式
语句是个烫手的山芋,因为它强制进行修改。表达式提升了不可变性和函数组合的能力。比如,我们先用for语句计算折扣后的总价。这样的代码导致了可变性以及冗长的代码。使用map和sum方法的表达性更强的声明式的版本后,不仅避免了修改操作,同时还能把函数串联起来。
写代码的时候应该尽量使用表达式,而不是语句。这样使得代码更简洁易懂。代码会顺着业务逻辑执行,就像我们描述问题的时候那样。如果需求变动,简洁的版本无疑更容易修改。
使用高阶函数进行设计
Java不像Haskell那些函数式语言那样强制要求不可变,它允许我们修改变量。因此,Java不是,也永远不会是,一个纯粹的函数式编程语言。然而,我们可以在Java里使用高阶函数进行函数式编程。
高阶函数使得重用更上一层楼。有了高阶函数我们可以很方便的重用那些小而专,内聚性强的成熟的代码。
在OOP中我们习惯了给方法传递给对象,在方法里面创建新的对象,然后返回对象。高阶函数对函数做的事情就跟方法对对象做的一样。有了高阶函数我们可以
- 把函数传给函数
- 在函数内创建新的函数
- 在函数内返回函数
我们已经见过一个把函数传参给另一个函数的例子了,在后面我们还会看到创建函数和返回函数的示例。我们先再看一遍“把函数传参给函数”的那个例子
prices.stream()
.filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0) .map(price -> price.multiply(BigDecimal.valueOf(0.9)))
report erratum ? discuss
.reduce(BigDecimal.ZERO, BigDecimal::add);
在这段代码中我们把函数price -> price.multiply(BigDecimal.valueOf(0.9)),传给了map函数。传递的这个函数是在调用高阶函数map的时候才创建的。通常来说一个函数有函数体,函数名,参数列表,返回值。这个实时创建的函数有一个参数列表后面跟着一个箭头(->),然后就是很短的一段函数体了。参数的类型由Java编译器来进行推导,返回的类型也是隐式的。这是个匿名函数,它没有名字。不过我们不叫它匿名函数,我们称之为lambda表达式。
匿名函数作为传参在Java并不算是什么新鲜事;我们之前也经常传递匿名内部类。即使匿名类只有一个方法,我们还是得走一遍创建类的仪式,然后对它进行实例化。有了lambda表达式我们可以享受轻量级的语法了。不仅如此,我们之前总是习惯把一些概念抽象成各种对象,现在我们可以将一些行为抽象成lambda表达式了。
用这种编码风格进行程序设计还是需要费些脑筋的。我们得把已经根深蒂固的命令式思维转变成函数式的。开始的时候可能有点痛苦,不过很快你就会习惯它了,随着不断的深入,那些非函数式的API逐渐就被抛到脑后了。
这个话题就先到这吧,我们来看看Java是如何处理lambda表达式的。我们之前总是把对象传给方法,现在我们可以把函数存储起来并传递它们。 我们来看下Java能够将函数作为参数背后的秘密。
第五节:加了点语法糖
用Java原有的功能也是可以实现这些的,不过lambda表达式加了点语法糖,省掉了一些步骤,使我们的工作更简单了。这样写出的代码不仅开发更快,也更能表达我们的想法。
过去我们用的很多接口都只有一个方法:像Runnable, Callable等等。这些接口在JDK库中随处可见,使用它们的地方通常用一个函数就能搞定。原来的这些只需要一个单方法接口的库函数现在可以传递轻量级函数了,多亏了这个通过函数式接口提供的语法糖。
函数式接口是只有一个抽象方法的接口。再看下那些只有一个方法的接口,Runnable,Callable等,都适用这个定义。JDK8里面有更多这类的接口——Function, Predicate, Comsumer, Supplier等(157页,附录1有更详细的接口列表)。函数式接口可以有多个static方法,和default方法,这些方法是在接口里面实现的。
我们可以用@FunctionalInterface注解来标注一个函数式接口。编译器不使用这个注解,不过有了它可以更明确的标识这个接口的类型。不止如此,如果我们用这个注解标注了一个接口,编译器会强制校验它是否符合函数式接口的规则。
如果一个方法接收函数式接口作为参数,我们可以传递的参数包括:
- 匿名内部类,最古老的方式
- lambda表达式,就像我们在map方法里那样
- 方法或者构造器的引用(后面我们会讲到)
如果方法的参数是函数式接口的话,编译器会很乐意接受lambda表达式或者方法引用作为参数。
如果我们把一个lambda表达式传递给一个方法,编译器会先把这个表达式转化成对应的函数式接口的一个实例。这个转化可不止是生成一个内部类而已。同步生成的这个实例的方法对应于参数的函数式接口的抽象方法。比如,map方法接收函数式接口Function作为参数。在调用map方法时,java编译器会同步生成它,就像下图所示的一样。
lambda表达式的参数必须和接口的抽象方法的参数匹配。这个生成的方法将返回lambda表达式的结果。如果返回类型不直接匹配抽象方法的话,这个方法会把返回值转化成合适的类型。
我们已经大概了解了下lambda表达式是如何传递给方法的。我们先来快速回顾一下刚讲的内容,然后开始我们lambda表达式的探索之旅。
总结
这是Java一个全新的领域。通过高阶函数,我们现在可以写出优雅流利的函数式风格的代码了。这样写出的代码,简洁易懂,错误少,利于维护和并行化。Java编译器发挥了它的魔力,在接收函数式接口参数的地方,我们可以传入lambda表达式或者方法引用。
我们现在可以进入lambda表达式以及为之改造的JDK库的世界来感觉它们的乐趣了。在下一章中,我们将从编程里面最常见的集合操作开始,发挥lambda表达式的威力。
译注:终于搞完无聊的第一章了。下一章开始会有更多实际的例子了。
未完待续,后续文章请继续关注deepinmind。
原创文章转载请注明出处:http://it.deepinmind.com