第一章 你好,lambda表达式!
第一节
Java的编码风格正面临着翻天覆地的变化。
我们每天的工作将会变成更简单方便,更富表现力。Java这种新的编程方式早在数十年前就已经出现在别的编程语言里面了。这些新特性引入Java后,我们可以写出更简洁,优雅,表达性更强,错误更少的代码。我们可以用更少的代码来实现各种策略和设计模式。
在本书中我们将通过日常编程中的一些例子来探索函数式风格的编程。在使用这种全新的优雅的方式进行设计编码之前,我们先来看下它到底好在哪里。
改变了你的思考方式
命令式风格——Java语言从诞生之初就一直提供的是这种方式。使用这种风格的话,我们得告诉Java每一步要做什么,然后看着它切实的一步步执行下去。这样做当然很好,就是显得有点初级。代码看起来有点啰嗦,我们希望这个语言能变得稍微智能一点;我们应该直接告诉它我们想要什么,而不是告诉它如何去做。好在现在Java终于可以帮我们实现这个愿望了。我们先来看几个例子,了解下这种风格的优点和不同之处。
正常的方式
我们先从两个熟悉的例子来开始。这是用命令的方式来查看芝加哥是不是指定的城市集合里——记住,本书中列出的代码只是部分片段而已。
boolean found = false;
for(String city : cities) {
if(city.equals("Chicago")) {
found = true;
break;
}
}
System.out.println("Found chicago?:" + found);
这个命令式的版本看起来有点啰嗦而且初级;它分成好几个执行部分。先是初始化一个叫found的布尔标记,然后遍历集合里的每一个元素;如果发现我们要找的城市了,设置下这个标记,然后跳出循环体;最后打印出查找的结果。
一种更好的方式
细心的Java程序员看完这段代码后,很快会想到一种更简洁明了的方式,就像这样:
System.out.println("Found chicago?:" + cities.contains("Chicago"));
这也是一种命令式风格的写法——contains方法直接就帮我们搞定了。
实际改进的地方
代码这么写有这几个好处:
1.不用再捣鼓那个可变的变量了
2.将迭代封装到了底层
3.代码更简洁
4.代码更清晰,更聚焦
5.少走弯路,代码和业务需求结合更密切
6.不易出错
7.易于理解和维护
来个复杂点的例子
这个例子太简单了,命令式查询一个元素是否存在于某个集合在Java里随处可见。现在假设我们要用命令式编程来进行些更高级的操作,比如解析文件 ,和数据库交互,调用WEB服务,并发编程等等。现在我们用Java可以写出更简洁优雅同时出错更少的代码,更不只是这种简单的场景。
老的方式
我们来看下另一个例子。我们定义了一系列价格,并通过不同的方式来计算打折后的总价。
final List<BigDecimal> prices = Arrays.asList(
new BigDecimal("10"), new BigDecimal("30"), new BigDecimal("17"),
new BigDecimal("20"), new BigDecimal("15"), new BigDecimal("18"),
new BigDecimal("45"), new BigDecimal("12"));
假设超过20块的话要打九折,我们先用普通的方式实现一遍。
BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO;
for(BigDecimal price : prices) {
if(price.compareTo(BigDecimal.valueOf(20)) > 0)
totalOfDiscountedPrices =
totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9)));
}
System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
这个代码很熟悉吧;先用一个变量来存储总价;然后遍历所有的价格,找出大于20块的,算出它们的折扣价,并加到总价里面;最后打印出折扣后的总价。
下面是程序的输出:
Total of discounted prices: 67.5
结果完全正确,不过这样的代码有点乱。这并不是我们的错,我们只能用已有的方式来写。不过这样的代码实在有点初级,它不仅存在基本类型偏执,而且还违反了单一职责原则。如果你是在家工作并且家里还有想当码农的小孩的话,你可得把你的代码藏好了,万一他们看见了会很失望地叹气道,“你是靠这些玩意儿糊口的?”
还有更好的方式
我们还能做的更好——并且要好很多。我们的代码有点像需求规范。这样能缩小业务需求和实现的代码之间的差距,减少了需求被误读的可能性。
我们不再让Java去创建一个变量然后没完没了的给它赋值了,我们要从一个更高层次的抽象去与它沟通,就像下面的这段代码。
final BigDecimal totalOfDiscountedPrices =
prices.stream()
.filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
.map(price -> price.multiply(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
大声的读出来吧——过滤出大于20块的价格,把它们转化成折扣价,然后加起来。这段代码和我们描述需求的流程简直一模一样。Java里还可以很方便的把一行长的代码折叠起来,根据方法名前面的点号进行按行对齐,就像上面那样。
代码非常简洁,不过我们用到了Java8里面的很多新东西。首先,我们调用 了价格列表的一个stream方法。这打开了一扇大门,门后边有数不尽的便捷的迭代器,这个我们在后面会继续讨论。
我们用了一些特殊的方法,比如filter和map,而不是直接的遍历整个列表。这些方法不像我们以前用的JDK里面的那些,它们接受一个匿名的函数——lambda表达式——作为参数。(后面我们会深入的展开讨论)。我们调用reduce()方法来计算map()方法返回的价格的总和。
就像contains方法那样,循环体被隐藏起来了。不过map方法(以及filter方法)则更复杂得多 。它对价格列表中的每一个价格,调用了传进来的lambda表达式进行计算,把结果放到一个新的集合里面。最后我们在这个新的集合上调用 reduce方法得出最终的结果。
这是以上代码的输出结果:
Total of discounted prices: 67.5
改进的地方
这和前面的实现相比改进明显:
1.结构良好而不混乱
2.没有低级操作
3.易于增强或者修改逻辑
4.由方法库来进行迭代
5.高效;循环体惰性求值
6.易于并行化
下面我们会说到Java是如何实现这些的。
lambda表达式来拯救世界了
lambda表达式是让我们远离命令式编程烦恼的快捷键。Java提供的这个新特性,改变了我们原有的编程方式,使得我们写出的代码不仅简洁优雅,不易出错,而且效率更高,易于优化改进和并行化。
第二节:函数式编程的最大收获
函数式风格的代码有更高的信噪比;写的代码更少了,但每一行或者每个表达式做的却更多了。比命令式编程相比,函数式编程让我们获益良多:
避免了对变量的显式的修改或赋值,这些通常是BUG的根源,并导致代码很难并行化。在命令行编程中我们在循环体内不停的对totalOfDiscountedPrices变量赋值。在函数式风格里,代码不再出现显式的修改操作。变量修改的越少,代码的BUG就越少。
函数式风格的代码可以轻松的实现并行化。如果计算很费时,我们可以很容易让列表中的元素并发的执行。如果我们想把命令式的代码并行化,我们还得担心并发修改totalOfDiscountedPrices变量带来的问题。在函数式编程中我们只会在完全处理完后才访问这个变量,这样就消除了线程安全的隐患。
代码的表达性更强。命令式编程要分成好几个步骤要说明要做什么——创建一个初始化的值,遍历价格,把折扣价加到变量上等等——而函数式的话只需要让列表的map方法返回一个包括折扣价的新的列表然后进行累加就可以了。
函数式编程更简洁;和命令式相比同样的结果只需要更少的代码就能完成。代码更简洁意味着写的代码少了,读的也少了,维护的也少了——看下第7页的"简洁少就是简洁了吗"。
函数式的代码更直观——读代码就像描述问题一样——一旦我们熟悉语法后就很容易能看懂。map方法对集合的每个元素都执行了一遍给定的函数(计算折扣价),然后返回结果集,就像下图演示的这样。
图1——map对集合中的每个元素执行给定的函数
有了lambda表达式之后,我们可以在Java里充分发挥函数式编程的威力。使用函数式风格,就能写出表达性更佳,更简洁,赋值操作更少,错误更少的代码了。
支持面向对象编程是Java一个主要的优点。函数式编程和面向对象编程并不排斥。真正的风格变化是从命令行编程转到声明式编程。在Java 8里,函数式和面向对象可以有效的融合到一起。我们可以继续用OOP的风格来对领域实体以及它们的状态,关系进行建模。除此之外,我们还可以对行为或者状态的转变,工作流和数据处理用函数来进行建模,建立复合函数。
第三节:为什么要用函数式风格?
我们看到了函数式编程的各项优点,不过使用这种新的风格划得来吗?这只是个小改进还是说换头换面?在真正在这上面花费工夫前,还有很多现实的问题需要解答。
小明问到:
代码少就是简洁了吗?
简洁是少而不乱,归根结底是说要能有效的表达意图。它带来的好处意义深远。
写代码就好像把配料堆到一起,简洁就是说能把配料调成调料。要写出简洁的代码可得下得狠工夫。读的代码是少了,真正有用的代码对你是透明的。一段很难理解或者隐藏细节的短代码只能说是简短而不是简洁。
简洁的代码竟味着敏捷的设计。简洁的代码少了那些繁文缛节。这是说我们可以对想法进行快速尝试,如果不错就继续,如果效果不佳就迅速跳过。
用Java写代码并不难,语法简单。而且我们也已经对现有的库和API很了如指掌了。真正难的是要拿它来开发和维护企业级的应用。
我们要确保同事在正确的时间关闭了数据库连接,还有他们不会不停的占有事务,能在合适的分层上正确的处理好异常,能正确的获得和释放锁,等等。
这些问题任何一个单独来看都不是什么大事。不过如果和领域内的复杂性一结合的话,问题就变得很棘手了,开发资源紧张,难以维护。
如果把这些策略封装成许多小块的代码,让它们各自进行约束管理的话,会怎么样呢?那我们就不用再不停的花费精力去实施策略了。这是个巨大的改进, 我们来看下函数式编程是如何做到的。
疯狂的迭代
我们一直都在写各种迭代来处理列表,集合,还有map。在Java里使用迭代器再常见不过了,不过这太复杂了。它们不仅占用了好几行代码,还很难进行封装。
我们是如何遍历集合并打印它们的?可以使用一个for循环。我们怎么从集合里过滤出一些元素?还是用for循环,不过还需要额外增加一些可修改的变量。选出了这些值后,怎么用它们求出最终值,比如最小值,最大值,平均值之类的?那还得再循环,再修改变量。
这样的迭代就是个万金油,啥都会点,但样样稀松。现在Java为许多操作都专门提供了内建的迭代器:比如只做循环的,还有做map操作的,过滤值的,做reduce操作的,还有许多方便的函数比如 最大最小值,平均值等等。除此之外,这些操作还可以很好的组合起来,因此我们可以将它们拼装到一起来实现业务逻辑,这样做既简单代码量也少。而且写出来的代码可读性强,因为它从逻辑上和描述问题的顺序是一致的。我们在第二章,集合的使用,第19页会看到几个这样的例子,这本书里这样的例子也比比皆是。
应用策略
策略贯穿于整个企业级应用中。比如,我们需要确认某个操作已经正确的进行了安全认证,我们要保证事务能够快速执行,并且正确的更新修改日志。这些任务通常最后就变成服务端的一段普通的代码,就跟下面这个伪代码差不多:
Transaction transaction = getFromTransactionFactory();
//... operation to run within the transaction ...
checkProgressAndCommitOrRollbackTransaction();
UpdateAuditTrail();
这种处理方法有两个问题。首先,它通常导致了重复的工作量并且还增加了维护的成本。第二,很容易忘了业务代码中可能会被抛出来的异常,可能会影响到事务的生命周期和修改日志的更新。这里应该使用try, finally块来实现,不过每当有人动了这块代码,我们又得重新确认这个策略没有被破坏。
还有一种方法,我们可以去掉工厂,把这段代码放在它前面。不用再获取事务对象,而是把执行的代码传给一个维护良好的函数,就像这样:
runWithinTransaction((Transaction transaction) -> {
//... operation to run within the transaction ...
});
这是你的一小步,但是省了一大堆事。检查状态同时更新日志的这个策略被抽象出来封装到了runWithinTransaction方法里。我们给这个方法发送一段需要在事务上下文里运行的代码。我们不用再担心谁忘了执行这个步骤或者没有处理好异常。这个实施策略的函数已经把这事搞定了。
我们将会在第五章中介绍如果使用lambda表达式来应用这样的策略。
扩展策略
策略看起来无处不在。除了要应用它们外,企业级应用还需要对它们进行扩展。我们希望能通过一些配置信息来增加或者删除一些操作,换言之,就是能在模块的核心逻辑执行前进行处理。这在Java里很常见,不过需要预先考虑到并设计好。
需要扩展的组件通常有一个或者多个接口。我们需要仔细设计接口以及实现类的分层结构。这样做可能效果很好,但是会留下一大堆需要维护的接口和类。这样的设计很容易变得笨重且难以维护,最终破坏扩展的初衷。
还有一种解决方法——函数式接口,以及lambda表达式,我们可以用它们来设计可扩展的策略。我们不用非得创建新的接口或者都遵循同一个方法名,可以更聚焦要实现的业务逻辑,我们会在73页的使用lambda表达式进行装饰中提到。
轻松实现并发
一个大型应用快到了发布里程碑的时候,突然一个严重的性能问题浮出水面。团队迅速定位出性能瓶颈点是出在一个处理海量数据的庞大的模块里。团队中有人建议说如果能充分发掘多核的优势的话可以提高系统性能。不过如果这个庞大的模块是用老的Java风格写的话,刚才这个建议带来的喜悦很快就破灭了。
团队很快意识到要这把这个庞然大物从串行执行改成并行需要费很大的精力,增加了额外的复杂度,还容易引起多线程相关的BUG。难道没有一种提高性能的更好方式吗?
有没有可能串行和并行的代码都是一样的,不管选择串行还是并行执行,就像按一下开关,表明一下想法就可以了?
听起来好像只有纳尼亚里面能这样,不过如果我们完全用函数式进行开发的话,这一切都将成为现实。内置的迭代器和函数式风格将扫清通往并行化的最后一道障碍。JDK的设计使得串行和并行执行的切换只需要一点不起眼的代码改动就可以实现,我们将会在145页《完成并行化的飞跃》中提到。
讲故事
在业务需求变成代码实现的过程中会丢失大量的东西。丢失的越多,出错的可能性和管理的成本就越高。如果代码看起来就跟描述需求一样,将会很方便阅读,和需求人员讨论也变的更简单,也更容易满足他们的需求。
比如你听到产品经理在说,”拿到所有股票的价格,找出价格大于500块的,计算出能分红的资产总和”。使用Java提供的新设施,可以这么写:
tickers.map(StockUtil::getprice).filter(StockUtil::priceIsLessThan500).sum()
这个转化过程几乎是无损的,因为基本上也没什么要转化的。这是函数式在发挥作用,在本书中还会看到更多这样的例子,尤其是第8章,使用lambda表达式来构建程序,137页。
关注隔离
在系统开发中,核心业务和它所需要的细粒度逻辑通常需要进行隔离。比如说,一个订单处理系统想要对不同的交易来源使用不同的计税策略。把计税和其余的处理逻辑进行隔离会使得代码重用性和扩展性更高。
在面向对象编程中我们把这个称之为关注隔离,通常用策略模式来解决这个问题。解决方法一般就是创建一些接口和实现类。
我们可以用更少的代码来完成同样的效果。我们还可以快速尝试自己的产品思路,而不用上来就得搞出一堆代码,停滞不前。我们将在63页的,使用lambda表达式进行关注隔离中进一步探讨如果通过轻量级函数来创建这种模式以及进行关注隔离。
惰性求值
开发企业级应用时,我们可能会与WEB服务进行交互,调用数据库,处理XML等等。我们要执行的操作有很多,不过并不是所有时候都全部需要。避免某些操作或者至少延迟一些暂时不需要的操作是提高性能或者减少程序启动,响应时间的一个最简单的方式。
这只是个小事,但用纯OOP的方式来实现还需要费一番工夫。为了延迟一些重量级对象的初始化,我们要处理各种对象引用 ,检查空指针等等。
不过,如果使用了新的Optinal类和它提供的一些函数式风格的API,这个过程将变得很简单,代码也更清晰明了,我们会在105页的延迟初始化中讨论这个。
提高可测性
代码的处理逻辑越少,容易被改错的地方当然也越少。一般来说函数式的代码比较容易修改,测试起来也较简单。
另外,就像第4章,使用lambda表达式进行设计和第5章资源的使用中那样,lambda表达式可以作为一种轻量级的mock对象,让异常测试变得更清晰易懂。lambda表达式还可以作为一个很好的测试辅助工具。很多常见的测试用例都可以接受并处理lambda表达式。这样写的测试用例能够抓住需要回归测试的功能的本质。同时,需要测试的各种实现都可以通过传入不同的lambda表达式来完成。
JDK自己的自动化测试用例也是lambda表达式的一个很好的应用范例——想了解更多的话可以看下OpenJDK仓库里的源代码。通过这些测试程序可以看到lambda表达式是如何将测试用例的关键行为进行参数化;比如,它们是这样构建测试程序的,“新建一个结果的容器”,然后“对一些参数化的后置条件进行检查”。
我们已经看到,函数式编程不仅能让我们写出高质量的代码,还能优雅的解决开发过程中的各种难题。这就是说,开发程序将变得更快更简单,出错也更少——只要你能遵守我们后面将要介绍到的几条准则。
第四节:进化而非革命
我们用不着转向别的语言,就能享受函数式编程带来的好处;需要改变的只是使用Java的一些方式。C++,Java,C#这些语言都支持命令式和面向对象的编程。不过现在它们都开始投入函数式编程的怀抱里了。我们刚才已经看到了这两种风格的代码,并讨论了函数式编程能带来的好处。现在我们来看下它的一些关键概念和例子来帮助我们学习这种新的风格。
Java语言的开发团队花费了大量的时间和精力把函数式编程的能力添加到了Java语言和JDK里。要享受它带来的好处,我们得先介绍几个新的概念。我们只要遵循下面几条规则就能提升我们的代码质量:
1.声明式
2.提倡不可变性
3.避免副作用
4.优先使用表达式而不是语句
5.使用高阶函数进行设计
我们来看下这几条实践准则。
声明式
我们所熟悉的命令式编程的核心就是可变性和命令驱动的编程。我们创建变量,然后不断修改它们的值。我们还提供了要执行的详细的指令,比如生成迭代的索引标志,增加它的值,检查循环是否结束,更新数组的第N个元素等。在过去由于工具的特性和硬件的限制,我们只能这么写代码。 我们也看到了,在一个不可变集合上,声明式的contains方法比命令式的更容易使用。所有的难题和低级的操作都在库函数里来实现了,我们不用再关心这些细节。就冲着简单这点,我们也应该使用声明式编程。不可变性和声明式编程是函数式编程的精髓,现在Java终于把它变成了现实。
提倡不可变性
变量可变的代码会有很多活动路径。改的东西越多,越容易破坏原有的结构,并引入更多的错误。有多个变量被修改的代码难于理解也很难进行并行化。不可变性从根本上消除了这些烦恼。 Java支持不可变性但没有强制要求——但我们可以。我们需要改变修改对象状态这个旧习惯。我们要尽可能的使用不可变的对象。 声明变量,成员和参数的时候,尽量声明为final的,就像Joshua Bloch在” Effective Java“里说的那句名言那样,“把对象当成不可变的吧”。 当创建对象的时候,尽量创建不可变的对象,比如String这样的。创建集合的时候,也尽量创建不可变或者无法修改的集合,比如用Arrays.asList()和Collections的unmodifiableList()这样的方法。 避免了可变性我们才可以写出纯粹的函数——也就是,没有副作用的函数。
避免副作用
假设你在写一段代码到网上去抓取一支股票的价格然后写到一个共享变量里。如果我们有很多价格要抓取,我们得串行的执行这些费时的操作。如果我们想借助多线程的能力,我们得处理线程和同步带来的麻烦事,防止出现竞争条件。最后的结果是程序的性能很差,为了维护线程而废寝忘食。如果消除了副作用,我们完全可以避免这些问题。 没有副作用的函数推崇的是不可变性,在它的作用域内不会修改任何输入或者别的东西。这种函数可读性强,错误少,容易优化。由于没有副作用,也不用再担心什么竞争条件或者并发修改了。不仅如此,我们还可以很容易并行执行这些函数,我们将在145页的来讨论这个。
优先使用表达式
语句是个烫手的山芋,因为它强制进行修改。表达式提升了不可变性和函数组合的能力。比如,我们先用for语句计算折扣后的总价。这样的代码导致了可变性以及冗长的代码。使用map和sum方法的表达性更强的声明式的版本后,不仅避免了修改操作,同时还能把函数串联起来。 写代码的时候应该尽量使用表达式,而不是语句。这样使得代码更简洁易懂。代码会顺着业务逻辑执行,就像我们描述问题的时候那样。如果需求变动,简洁的版本无疑更容易修改。
使用高阶函数进行设计
Java不像Haskell那些函数式语言那样强制要求不可变,它允许我们修改变量。因此,Java不是,也永远不会是,一个纯粹的函数式编程语言。然而,我们可以在Java里使用高阶函数进行函数式编程。 高阶函数使得重用更上一层楼。有了高阶函数我们可以很方便的重用那些小而专,内聚性强的成熟的代码。 在OOP中我们习惯了给方法传递给对象,在方法里面创建新的对象,然后返回对象。高阶函数对函数做的事情就跟方法对对象做的一样。有了高阶函数我们可以。
1.把函数传给函数
2.在函数内创建新的函数
3.在函数内返回函数
我们已经见过一个把函数传参给另一个函数的例子了,在后面我们还会看到创建函数和返回函数的示例。我们先再看一遍“把函数传参给函数”的那个例子:
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注解来标注一个函数式接口。编译器不使用这个注解,不过有了它可以更明确的标识这个接口的类型。不止如此,如果我们用这个注解标注了一个接口,编译器会强制校验它是否符合函数式接口的规则。 如果一个方法接收函数式接口作为参数,我们可以传递的参数包括:
1.匿名内部类,最古老的方式
2.lambda表达式,就像我们在map方法里那样
3.方法或者构造器的引用(后面我们会讲到)
如果方法的参数是函数式接口的话,编译器会很乐意接受lambda表达式或者方法引用作为参数。 如果我们把一个lambda表达式传递给一个方法,编译器会先把这个表达式转化成对应的函数式接口的一个实例。这个转化可不止是生成一个内部类而已。同步生成的这个实例的方法对应于参数的函数式接口的抽象方法。比如,map方法接收函数式接口Function作为参数。在调用map方法时,java编译器会同步生成它,就像下图所示的一样。
lambda表达式的参数必须和接口的抽象方法的参数匹配。这个生成的方法将返回lambda表达式的结果。如果返回类型不直接匹配抽象方法的话,这个方法会把返回值转化成合适的类型。 我们已经大概了解了下lambda表达式是如何传递给方法的。我们先来快速回顾一下刚讲的内容,然后开始我们lambda表达式的探索之旅。
总结
这是Java一个全新的领域。通过高阶函数,我们现在可以写出优雅流利的函数式风格的代码了。这样写出的代码,简洁易懂,错误少,利于维护和并行化。Java编译器发挥了它的魔力,在接收函数式接口参数的地方,我们可以传入lambda表达式或者方法引用。 我们现在可以进入lambda表达式以及为之改造的JDK库的世界来感觉它们的乐趣了。在下一章中,我们将从编程里面最常见的集合操作开始,发挥lambda表达式的威力。