这篇文章主要介绍了 Java 函数式编程(五): 闭包, 本文是系列文章的第 5 篇, 其它篇章请参阅相关文章, 需要的朋友可以参考下
使用词法作用域和闭包
很多开发人员都存在这种误解, 认为使用 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 作为参数也是情理之中的事情了
可以看到, 从集合中选取元素非常简单下面我们将介绍下如何从集合中只挑选出一个元素
来源: http://www.phperz.com/article/16/0714/235768.html