Java函数式编程(五):闭包

使用词法作用域和闭包

很多开发人员都存在这种误解,认为使用lambda表达式会导致代码冗余,降低代码质量。恰恰相反,就算代码变得再复杂,我们也不会为了代码的简洁性而在代码质量上做任何妥协,下面我们就会看到。

在前面一个例子中我们已经可以重用lambda表达式了;然而,如果再匹配另外一个字母,代码冗余的问题很快又卷土重来了。我们先来进一步分析下这个问题,然后再用词法作用域和闭包来把它解决掉。

lambda表达式带来的冗余

我们来从friends中过滤出那些以N或者B开头的字母。继续延用上面的那个例子,我们写出的代码可能会是这样的:

代码如下:

final Predicate<String> startsWithN = name -> name.startsWith("N");
final Predicate<String> startsWithB = name -> name.startsWith("B");
final long countFriendsStartN =
friends.stream()
.filter(startsWithN).count();
final long countFriendsStartB =
friends.stream()
.filter(startsWithB).count();

第一个predicate判断名字是否是以N开头的,而第二个是判断是否以B开头的。我们把这两个实例分别传递给两次filter方法调用。这样看起来很合理,但是两个predicate产生了冗余,它们只是那个检查的字母不同而已。我们来看下如何能避免这种冗余。

使用词法作用域来避免冗余

第一个方案,我们可以把字母抽出来作为函数的参数,同时把这个函数传递给filter方法。这是个不错的方法,不过filter可不是什么函数都接受的。它只接受只有一个参数的函数,那个参数对应的就是集合中的元素,返回一个boolean值,它希望传进来的是一个Predicate。

我们希望有一个地方能把这个字母先缓存起来,一直到参数传递过来(在这里就是name这个参数)。下面来新建一个这样的函数。

代码如下:

public static Predicate<String> checkIfStartsWith(final String letter) {
return name -> name.startsWith(letter);
}

我们定义了一个静态函数checkIfStartsWith,它接收一个String参数,并且返回一个Predicate对象,它正好可以传递给filter方法,以便后面进行使用。不像前面看到的高阶函数那样是以函数作为参数的,这个方法返回的是一个函数。不过它也是一个高阶函数,这个我们在12页的进化,而非变革中已经提到过了。

checkIfStartsWith方法返回的Predicate对象和其它lambda表达式有些不同。在 return name -> name.startsWith(letter)语句中,我们很清楚name是什么,它是传入到lambda表达式中的参数。不过变量letter到底是什么?它是在这个匿名函数的域外边的,Java找到了定义这个lambda表达式的域,并发现了这个变量letter。这个就叫做词法作用域。词法作用域是个很有用的东西,它使得我们可以在一个用用域中缓存一个变量,以便后面在另一个上下文中进行使用。由于这个lambda表达式使用了它的定义域中的变量,这种情况也被称作闭包。关于词法作用域的访问限制,可以看下31页的词法作用域有什么限制吗?

词法作用域有什么限制吗?

在lambda表达式中,我们只能访问它的定义域中的final类型或者实际上是final类型的本地变量。
lambda表达式可能马上就会被调用,也可能延迟进行调用,或者从不同的线程发起调用。为了避免竞争冲突,我们访问的定义域中的本地变量,一旦初始化后是不允许进行修改的。任何修改操作都会导致编译异常。

标记成final后解决了这个问题,不过Java并不强迫我们一定要这么标记。事实上,Java看的是两点。一个是访问的这个变量必须要在定义它的方法中完成初始化,并且要在定义lambda表达式之前。第二,这些变量的值不能进行修改——也就是说,它们事实上就是final类型的,尽管没有这么标记。
无状态的lambda表达式是运行时常量,而那些使用了本地变量的lambda表达则会有额外的计算开销。

在调用filter方法的时候我们就可以用checkIfStartsWith方法返回的lambda表达式了,就像这样:

代码如下:

final long countFriendsStartN =
friends.stream() .filter(checkIfStartsWith("N")).count();
final long countFriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();

在调用filter方法之前 ,我们先调用了checkIfStartsWith()方法,把想要的字母传参进去。这个调用很快就返回了一个lambda表达式,然后我们把它传参给filter方法。

通过创建了一个高阶函数(这里是checkIfStartsWith)并且使用了词法作用域,我们成功的去除了代码中的冗余。我们不用再重复的判断name是不是以某个字母开头了。

重构,缩小作用域

在前面的例子中我们用了一个static方法,不过我们不希望用static方法来缓存变量,这样把我们的代码搞乱了。最好能把这个函数的作用域缩小到使用它的地方。我们可以用一个Function接口来实现这个。

代码如下:

final Function<String, Predicate<String>> startsWithLetter = (String letter) -> {
Predicate<String> checkStarts = (String name) -> name.startsWith(letter);
return checkStarts; };

这个lambda表达式取代了原来的static方法,它可以放到函数里面,在需要用到它之前定义一下就好了。startWithLetter变量引用的是一个入参是String,出参是Predicate的Function。

和使用static方法相比,这个版本简单多了,不过我们还可以对它继续重构让它更简洁点。从实际的角度看,这个函数和前面的static方法是一样的;它们都接收一个String返回一个Predicate。为了不显式的声明一个Predicate, 我们用一个lamdba表达式整个给替换掉。

代码如下:

final Function<String, Predicate<String>> startsWithLetter = (String letter) -> (String name) -> name.startsWith(letter);

我们把那些乱七八糟的东西给干掉了,但是我们还可以去掉类型声明,让它更简洁一点,Java编译器会根据上下文去做类型推导的。我们来看下改进后的版本。

代码如下:

final Function<String, Predicate<String>> startsWithLetter =
letter -> name -> name.startsWith(letter);

要适应这种简洁的语法可得下点工夫。如果它亮瞎了你的眼睛的话,先看看别的地方吧。我们已经完成了代码的重构,现在可以用它来替换掉原来的checkIfStartsWith()方法了,就像这样:

代码如下:

final long countFriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
final long countFriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();

在这节中我们用到了高阶函数。我们看到了如果把函数传递给另一个函数,如何在函数中创建函数,以及如何通过函数来返回一个函数。这些例子都展示了lambda表达式带来的简洁性和可重用性。

本节中我们充分发挥了Function和Predicate的作用,不过我们来看下它们两个到底有什么区别。Predicate接受一个类型为T的参数,返回一个boolean值来代表它对应的判断条件的真假。当我们需要做条件判断的时候,我们可以使用Predicateg来完成。像filter这类对元素进行筛选的方法都接收Predicate作为参数。而Funciton代表的是一个函数,它的入参是类型为T的变量,返回的是R类型的一个结果。它和只能返回boolean的Predicate相比要更加通用。只要是将输入转化成一个输出的,我们都可以使用Function,因此map使用Function作为参数也是情理之中的事情了。

可以看到,从集合中选取元素非常简单。下面我们将介绍下如何从集合中只挑选出一个元素。

(0)

相关推荐

  • 详解java模板和回调机制

    最近看spring的JDBCTemplete的模板方式调用时,对模板和回调产生了浓厚兴趣,查询了一些资料,做一些总结. 回调函数: 所谓回调,就是客户程序C调用服务程序S中的某个函数A,然后S又在某个时候反过来调用C中的某个函数B,对于C来说,这个B便叫做回调函数.回调函数只是一个功能片段,由用户按照回调函数调用约定来实现的一个函数.回调函数是一个工作流的一部分,由工作流来决定函数的调用(回调)时机.一般说来,C不会自己调用B,C提供B的目的就是让S来调用它,而且是C不得不提供.由于S并不知道C

  • 说明Java的传递与回调机制的代码示例分享

    java传值还是传引用 1.原始类型参数传递 public void badSwap(int var1, int var2) { int temp = var1; var1 = var2; var2 = temp; } 2.引用类型参数传递 public void tricky(Point arg1, Point arg2) { arg1.x = 100; arg1.y = 100; Point temp = arg1; arg1 = arg2; arg2 = temp; } public st

  • JavaSript中变量的作用域闭包的深入理解

    复制代码 代码如下: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <script type="text/javascript"> /* * 1.js中的变量都是公用的. 2.js中没有静态变量 3.闭包:函数内部可以调用函数外部的变量;反之,则不行 */ v

  • 再谈java回调函数

    又遇到了回调函数,这次打算写下来分享一下. 所谓回调函数,或者在面向对象语言里叫回调方法,简单点讲,就是回头在某个时间(事件发生)被调用的函数. 再详细点:就是一个函数A,作为参数,传入了另一个函数B,然后被B在某个时间调用. 这里可以有疑问了,既然是一个函数调用另一个函数,可以在函数体里面调用啊,为什么还要把函数作为参数传到另一个函数里被调用?何况还有一些语言(比如java)不支持把函数作为参数. 对的,确实可以在函数体里调用另一个函数,功能上好像是没差别的,但是这里有一个问题,就是你要调用的

  • 妙解Java中的回调机制(CallBack)

    前言 最近学习java,接触到了回调机制(CallBack).初识时感觉比较混乱,而且在网上搜索到的相关的讲解,要么一言带过,要么说的比较单纯的像是给CallBack做了一个定义.当然了,我在理解了回调之后,再去看网上的各种讲解,确实没什么问题.但是,对于初学的我来说,缺了一个循序渐进的过程. 回调是一种双向调用模式,什么意思呢,就是说,被调用方在被调用时也会调用对方,这就叫回调."If you call me, i will call back". 不理解?没关系,先看看这个可以说比

  • 理解Java当中的回调机制(翻译)

    你好,今天我要和大家分享一些东西,举例来说这个在JavaScript中用的很多.我要讲讲回调(callbacks).你知道什么时候用,怎么用这个吗?你真的理解了它在java环境中的用法了吗?当我也问我自己这些问题,这也是我开始研究这些的原因.这个背后的思想是控制反转( PS:维基百科的解释是控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度.)这个范例描述了框架(framework)的工作方式,也以"好莱坞原则:

  • Java内部类之间的闭包和回调详解

    前言 闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域.通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用城内,内部类有权操作所有的成员,包括private成员. Java最引人争议的问题之一就是,人们认为Java应该包含某种类似指针的机制,以允许回调(callback).通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象.如果回调是通

  • 详解 JAVA的回调机制CallBack

    序言 CallBack是回调的意思,熟悉Windows编程的人对"回调函数"这四个字一定不会陌生,但是Java程序员对它可能就不太了解了."回调函数"或者"回调方法"是软件设计与开发中一个非常重要的概念,掌握"回调函数"的思想对程序员来说(不管用哪种语言)是非常必要的. 最近学习java,接触到了回调机制(CallBack).初识时感觉比较混乱,而且在网上搜索到的相关的讲解,要么一言带过,要么说的比较单纯的像是给CallBac

  • 详解Java的闭包

    在2013年将发布的 JavaSE8 中将包含一个叫做 Lambda Project 的计划,在今年6月份的 JSR-335 草案中有描述. JSR-335 将闭包引入了 Java .闭包在现在的很多流行的语言中都存在,例如 C++.C# .闭包允许我们创建函数指针,并把它们作为参数传递.在这篇文章中,我们将粗略的看一遍Java8的特性,并介绍Lambda表达式.而且我将试着放一些样例程序来解释一些概念和语法. Java 编程语言给我们提供了接口的概念,接口里可以定义抽象的方法.接口定义了 AP

  • java 实现回调代码实例

    JAVA实现回调 熟悉MS-Windows和X Windows事件驱动设计模式的开发人员,通常是把一个方法的指针传递给事件源,当某一事件发生时来调用这个方法(也称为"回调").Java的面向对象的模型目前不支持方法指针,似乎不能使用这种方便的机制. Java支持interface,通过interface可以实现相同的回调.其诀窍就在于定义一个简单的interface,申明一个被希望回调的方法. 例如,假定当某一事件发生时会得到通知,我们可以定义一个interface: public i

随机推荐