169it科技资讯


当前位置:  编程技术>java/j2ee

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

    来源: 互联网  发布时间:2014-11-08

使用词法作用域和闭包

很多开发人员都存在这种误解,认为使用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作为参数也是情理之中的事情了。

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


    
相关技术文章:
    ▪Java函数式编程(六):Optional

     选取单个元素 直觉来说选取单个元素肯定会比选取多个要简单得多,不过这里也存在一些问题。我们先看下一般的做法的问题是什么,然后再看下如何用lambda表达式来解决它。 我们先新建一个方法来查找一个以特定字母开头的元素,然后打印出来。 代码如下: public static void pickName( final List<String> names, final String startingLetter) { String foundName = null; for(String name : names) { if(name.startsWith(startingLetter)) { foundName = name; break; } } 这个方法简......


    ▪Java函数式编程(七):MapReduce

     译注:map(映射)和reduce(归约,化简)是数学上两个很基础的概念,它们很早就出现在各类的函数编程语言里了,直到2003年Google将其发扬光大,运用到分布式系统中进行并行计算后,这个组合的名字才开始在计算机界大放异彩(那些函数式粉可能并不这么认为)。本文我们会看到Java 8在摇身一变支持函数式编程后,map和reduce组合的首次亮相(这里只是初步介绍,后续还会有针对它们的专题)。 对集合进行归约 现在为止我们已经介绍了几个操作集合的新技巧了:查找匹配元素,查找单个元素,集合转化。这些操作有一个共同点,它们都是......


    ▪Java函数式编程(八):字符串及方法引用

     第三章 字符串,比较器和过滤器 JDK引入的一些方法对写出函数式风格的代码很有帮助。JDK库里的一些的类和接口我们已经用得非常熟悉了,比如说String,为了摆脱以前习惯的那种老的风格,我们得主动寻找机会来使用这些新的方法。同样,当我们需要用到只有一个方法的匿名内部类时,我们现在可以用lambda表达式来替换它了,不用再像原来那样写的那么繁琐了。 本章我们会使用lambda表达式和方法引用来遍历字符串,实现Comparator接口,查看目录中的文件,监视文件及目录的变更。上一章中介绍的一些方法还将继续出现在这里,来帮助我们......


 
最新技术文章:
    ▪Java中使用开源库JSoup解析HTML文件实例

     HTML是WEB的核心,互联网中你看到的所有页面都是HTML,不管它们是由JavaScript,JSP,PHP,ASP或者是别的什么WEB技术动态生成的。你的浏览器会去解析HTML并替你去渲染它们。不过如果你需要自己在Java程序中解析HTML文档并查找某些元素,标签,属性或者检查某个特定的元素是否存在的话,那又该如何呢?如果你已经使用Java编程多年了,我相信你肯定试过去解析XML,也使用过类似DOM或者SAX这样的解析器,不过很有可能你从未进行过任何的HTML解析的工作。更讽刺的是,在Java应用中,很少会有需要你去解析HTML文档的时候,这里并不包括Servlet或者其它的Java WEB技术。更糟糕......


    ▪Java函数式编程(一):你好,Lambda表达式

     第一章 你好,lambda表达式! 第一节 Java的编码风格正面临着翻天覆地的变化。 我们每天的工作将会变成更简单方便,更富表现力。Java这种新的编程方式早在数十年前就已经出现在别的编程语言里面了。这些新特性引入Java后,我们可以写出更简洁,优雅,表达性更强,错误更少的代码。我们可以用更少的代码来实现各种策略和设计模式。 在本书中我们将通过日常编程中的一些例子来探索函数式风格的编程。在使用这种全新的优雅的方式进行设计编码之前,我们先来看下它到底好在哪里。 改变了你的思......


    ▪Java函数式编程(二):集合的使用

     第二章:集合的使用 我们经常会用到各种集合,数字的,字符串的还有对象的。它们无处不在,哪怕操作集合的代码要能稍微优化一点,都能让代码清晰很多。在这章中,我们探索下如何使用lambda表达式来操作集合。我们用它来遍历集合,把集合转化成新的集合,从集合中删除元素,把集合进行合并。 遍历列表 遍历列表是最基本的一个集合操作,这么多年来,它的操作也发生了一些变化。我们使用一个遍历名字的小例子,从最古老的版本介绍到现在最优雅的版本。 用下面的代码我们很容易创建一个不可变的名字的......


 


站内导航:


特别声明:169IT网站部分信息来自互联网,如果侵犯您的权利,请及时告知,本站将立即删除!

©2012-2017,169IT.COM,E-mail:www_169it_com#163.com(请将#改为@)

浙ICP备11055608号