你好,请你帮我解决一个函数?

第一章 你好,lambda表达式!

Java的编码风格正面临着翻天覆地的变化。

我们每天的工作将会变成更简单方便,更富表现力。Java这种新的编程方式早在数十年前就已经出现在别的编程语言里面了。这些新特性引入Java后,我们可以写出更简洁,优雅,表达性更强,错误更少的代码。我们可以用更少的代码来实现各种策略和设计模式。

在本书中我们将通过日常编程中的一些例子来探索函数式风格的编程。在使用这种全新的优雅的方式进行设计编码之前,我们先来看下它到底好在哪里。

命令式风格——Java语言从诞生之初就一直提供的是这种方式。使用这种风格的话,我们得告诉Java每一步要做什么,然后看着它切实的一步步执行下去。这样做当然很好,就是显得有点初级。代码看起来有点啰嗦,我们希望这个语言能变得稍微智能一点;我们应该直接告诉它我们想要什么,而不是告诉它如何去做。好在现在Java终于可以帮我们实现这个愿望了。我们先来看几个例子,了解下这种风格的优点和不同之处。

我们先从两个熟悉的例子来开始。这是用命令的方式来查看芝加哥是不是指定的城市集合里——记住,本书中列出的代码只是部分片段而已。

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注解来标注一个函数式接口。编译器不使用这个注解,不过有了它可以更明确的标识这个接口的类型。不止如此,如果我们用这个注解标注了一个接口,编译器会强制校验它是否符合函数式接口的规则。 如果一个方法接收函数式接口作为参数,我们可以传递的参数包括:

1.匿名内部类,最古老的方式
2.lambda表达式,就像我们在map方法里那样
3.方法或者构造器的引用(后面我们会讲到)

如果方法的参数是函数式接口的话,编译器会很乐意接受lambda表达式或者方法引用作为参数。 如果我们把一个lambda表达式传递给一个方法,编译器会先把这个表达式转化成对应的函数式接口的一个实例。这个转化可不止是生成一个内部类而已。同步生成的这个实例的方法对应于参数的函数式接口的抽象方法。比如,map方法接收函数式接口Function作为参数。在调用map方法时,java编译器会同步生成它,就像下图所示的一样。

lambda表达式的参数必须和接口的抽象方法的参数匹配。这个生成的方法将返回lambda表达式的结果。如果返回类型不直接匹配抽象方法的话,这个方法会把返回值转化成合适的类型。 我们已经大概了解了下lambda表达式是如何传递给方法的。我们先来快速回顾一下刚讲的内容,然后开始我们lambda表达式的探索之旅。

这是Java一个全新的领域。通过高阶函数,我们现在可以写出优雅流利的函数式风格的代码了。这样写出的代码,简洁易懂,错误少,利于维护和并行化。Java编译器发挥了它的魔力,在接收函数式接口参数的地方,我们可以传入lambda表达式或者方法引用。 我们现在可以进入lambda表达式以及为之改造的JDK库的世界来感觉它们的乐趣了。在下一章中,我们将从编程里面最常见的集合操作开始,发挥lambda表达式的威力。

5.1.5 函数的递归调用

在函数调用中,通常我们都是在一个函数中调用另外一个函数,以此来完成其中的某部分功能。例如,我们在main()主函数中调 用PowerSum()函数来计算两个数的平方和,而在PowerSum()函数中,又调用Power()函数和Add()函数来计算每个数的平方并将两 个平方加和起来成为最终的结果。除此之外,在C++中还存在另外一种特殊的函数调用方式,那就是在一个函数内部调用它自己本身,这种方式也被称为函数的递 归调用。

函数的递归调用,实际上是实现函数的一种特殊方式。当递归函数被调用的时候,会产生一个自己调用自己的循环,这个循环会不断地 递归进行下去,直到最后一次函数调用在特殊条件下,也就是满足了递归的终止条件,不再继续调用自身而是返回某个具体的结果数据。这时,所有调用这个函数的 上层函数会依次返回,直到我们最初对这个函数的调用返回,获得其结果数据。虽然函数的递归调用每次调用的都是自己,但是每次递归调用的条件,也即是函数参 数,往往有所不同。正是调用条件的变化,才有可能使函数满足终止条件并返回一个具体的结果数据,不再继续递归地调用自身,这也即是递归调用的终点。

函数的递归调用虽然形式上比较复杂,但是它在处理那些可以把一个大问题分解成一个已知的结果与另一个类似的小问题,需要重复多 次做相似的事情才能最终解决的问题时,因为函数的递归调用本身所表达的意义就是循环往复地做同一件事情,所以在处理这类问题上有着天然的优势。例如,我们 要统计某个字符在目标字符串中出现的次数。通常,我们的思路是用for循环遍历整个字符数组,然后逐个字符地进行匹配统计。而如果采用递归函数的思路来解 决这个问题,那么整个统计过程就变为:从目标字符串的开始位置查找这个字符,如果找到,那么字符出现的次数就成了已经找到的这一次加上在剩下的字符串中出 现的次数,在程序中我们可以用“1 + CountChar(pos+1, c)”来表示,其中“1”表示已经找到的字符出现一次,而“CountChar(pos+1, c)”则代表了字符在剩下的字符串中出现的次数,加起来刚好就是字符在整个字符串中出现的次数。这里的“CountChar(pos+1, c)”就是在变更开始条件后对CountChar()函数的递归调用,进行第二次查找与统计。第二次查找也会进行类似的查找统计过程,如果找到则会第三次 调用CountChar()函数继续向后继续查找统计。这个过程会不断地持续进行下去,直到最后满足递归的终止条件——查找到了字符串的结尾,再也找不到 这个字符——为止。在这个过程中,有需要循环往复执行的相同动作——从字符串开始位置查找目标字符;有不同的开始条件——在字符串的不同位置开始查找;有 终止条件——在字符串中再也找不到目标字符。有了这三个特征,我们就可以用函数的递归调用更轻松而自然地解决这个问题:

// 用函数的递归调用实现统计字符在字符串中出现的次数 // 从字符串str的开始位置查找字符c // 在字符串中再也找不到目标字符,递归的终止条件得到满足 // 则结束函数的递归调用,直接返回本次的查找结果0 // 如果没有达到终止条件,则将本次查找结果1统计在内, // 并在新的开始位置pos + 1开始下一次查找,实现函数的递归调用

在执行的过程中,当CountChar()在主函数中第一次被调用时,第一个参数str指向的字符串是“Thought is a seed”,这时进入CountChar()函数执行,strchr()函数会在其中找到字符‘h’出现的位置并保存到字符指针pos中,此时尚不满足终 止条件(nullprt == pos), 则执行“return 1 + CountChar(pos+1,c)”,将本次查找结果统计在内,并变更递归的开始条件为“pos+1”,让第二次递归调用CountChar()函数 时参数str指向的字符串变为“ought is a seed”。在第二次进入CountChar()函数执行时,strchr()函数会找到字符‘h’第二次出现的位置,递归的终止条件依然无法得到满足, 则继续将本次查找结果统计在内并修改开始条件,将CountChar()函数的str参数指向“t is a seed”,开始第三次递归调用。在第三次进入CountChar()函数执行时,strchr()函数在剩下的字符串中再也找不到目标字符,递归的终止 条件得到满足,函数直接返回本次的查找统计结果0(return 0;),不再继续向下递归调用CountChar()函数,然后逐层向上返回,最终结束整个函数递归调用的过程,得到最终结果2,也就是目标字符在字符串 中出现的次数。整个过程如下图5-8所示。

函数的递归调用,其实质就是将一个大问题不断地分解成多个相似的小问题,然后通过不断地细分,直到小问题被解决,才最终解决最 开始的大问题。例如在这个例子中,我们开始的大问题是统计字符串中的目标字符的个数,然后这个大问题被分解为当前已经找到的目标字符数1和剩余字符串中的 目标字符数CountChar(pos+1,c),而我们要计算剩余字符串中的目标字符数,又可以采用同样的策略进一步细分,直至剩余字符串中没有目标字 符,无法继续细分为止。从这里我们也可以看到,函数的递归调用实际上是一个循环过程,我们必须确保函数能够达到它的递归终止条件,结束递归。例如,我们这 里不断地调整查找的开始位置,让查找到最后再也无法找到目标字符而满足终止条件。否则,函数会无限地递归调用下去,最终形成一个无限循环而永远无法获得结 果。这一点是我们在设计递归函数时尤其需要注意的。

函数的递归调用,是通过在一个函数中循环往复地调用它自身来完成的,从本质上讲,函数的递归调用其实是一种特殊形式的循环。所以,我们也可以将一个函数的递归调用改用循环结构来实现。例如,上面的CountChar()函数可以用循环结构改写为:

// 用循环结构实现统计字符在字符串中出现的次数
 // 在字符串中查找字符,并对结果进行判断
 ++str; // 字符串往后移动,开始下一次循环
 

这里我们不禁要问,既然函数的递归调用可以用循环结构来实现,而函数的递归调用又涉及到函数调用时的那些传递参数保护现场的幕 后工作,性能比较低下,那么我们为什么还要使用函数的递归调用而不是直接使用效率更高的循环结构来解决问题呢?这是因为,面对某些特殊问题,我们很难用循 环结构来解决。比如,从一个数组中找出连续和值最大的数据序列,如果采用循环结构,我们几乎无从下手,即使最后解决了但性能也是十分低下。而恰好这种问题 又可以细分成多个类似的小问题,比如这里我们可以将数组分成左右两部分,那么和值最大的数据序列要么在左边部分,要么在右边部分,要么跨越两个部分。这 样,这个问题就细分成了寻找左边部分、右边部分和跨越左右部分的和值最大序列的三个相似的小问题。而这三个小问题又可以进一步细化,直至最后可以轻松解决 的最小问题。在这种情况下使用函数的递归调用来解决问题,更加符合我们人类的思考方式,问题解决起来更加容易,同时其性能也会优于循环结构的实现,做到了 “又好又快”。解决这类可以不断细分的特殊问题,就是函数递归调用的用武之地。

我要回帖

更多关于 请写出你知道的UFO报表的函数 的文章

 

随机推荐