使用词法作用域和闭包
很多开发人员都存在这种误解,认为使用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页的
词法作用域有什么限制吗?
<blockquote>
词法作用域有什么限制吗?
<br>
在lambda表达式中,我们只能访问它的定义域中的final类型或者实际上是final类型的本地变量。
<br>
lambda表达式可能马上就会被调用,也可能延迟进行调用,或者从不同的线程发起调用。为了避免竞争冲突,我们访问的定义域中的本地变量,一旦初始化后是不允许进行修改的。任何修改操作都会导致编译异常。
<br>
标记成final后解决了这个问题,不过Java并不强迫我们一定要这么标记。事实上,Java看的是两点。一个是访问的这个变量必须要在定义它的方法中完成初始化,并且要在定义lambda表达式之前。第二,这些变量的值不能进行修改——也就是说,它们事实上就是final类型的,尽管没有这么标记。
<br>
无状态的lambda表达式是运行时常量,而那些使用了本地变量的lambda表达则会有额外的计算开销。
</blockquote>
在调用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>接受一个类型为T的参数,返回一个boolean值来代表它对应的判断条件的真假。当我们需要做条件判断的时候,我们可以使用Predicateg来完成。像filter这类对元素进行筛选的方法都接收Predicate作为参数。而Funciton<T, R>代表的是一个函数,它的入参是类型为T的变量,返回的是R类型的一个结果。它和只能返回boolean的Predicate相比要更加通用。只要是将输入转化成一个输出的,我们都可以使用Function,因此map使用Function作为参数也是情理之中的事情了。
可以看到,从集合中选取元素非常简单。下面我们将介绍下如何从集合中只挑选出一个元素。
未完待续,后续文章请继续关注
Java译站。
原创文章转载请注明出处:
http://it.deepinmind.com
分享到:
相关推荐
对于已经熟悉Java的开发者来说,学习JavaScript函数式编程不仅可以拓宽视野,也有助于提升在前端开发领域的专业技能。阅读相关书籍,例如"javascript函数式编程",将帮助你更好地掌握这些概念并应用于实际项目中。
#### 七、函数式编程的关键概念 - **Closure**(闭包):闭包是指函数能够访问其定义时所在作用域中的变量。 - **Continuation**(延续):表示程序的控制流的某一点,可以被捕获并在稍后恢复执行。 - **Currying**...
### 面向Java开发者的函数式编程 #### 书籍概览 《面向Java开发者的函数式编程》是由Dean Wampler编写的一本专业书籍,旨在为Java开发者介绍函数式编程(Functional Programming,简称FP)的基本原理及其在Java中...
Java函数式编程中的闭包是一种强大的工具,它允许函数访问并操作其定义时的作用域内的变量,即使该函数被传递到其他上下文中。在描述的示例中,我们看到闭包是如何帮助消除代码冗余的。 当我们需要创建多个类似的...
闭包在函数式编程中用于创建私有变量和实现封装。 4. **柯里化**:柯里化是一种将接受多个参数的函数转换为接受单一参数并返回新函数的技术。这种技术有助于实现函数的组合和部分应用。 5. **函数组合**:函数组合...
虽然Java本身不是一种函数式编程语言,但是JVM强大的生态系统为运行函数式编程语言提供了坚实的基础。 本书的作者Michael Bevilacqua-Linn通过自己的经验,结合了多个编程实践者的点评,将关于Scala和Clojure中函数...
10. **在面向对象语言中的函数式编程**:即使在非函数式语言如Java或C#中,也可以引入函数式编程的概念,例如使用lambda表达式和Stream API,以增加代码的灵活性。 通过《实用函数式编程研讨会》,参与者将有机会...
- **C#**、**Java** 和 **JavaScript** 需要更多努力来编写纯函数,但它们也提供了对函数式编程的支持。 【函数式编程技术】 1. **头等函数(first-class functions)**:函数可以作为值传递、存储和返回,使得函数...
然而,Scala 作为原生支持函数式编程的语言,其语法更简洁,对高阶函数和闭包的处理更为自然。在选择语言时,开发者应根据项目需求、团队技能和性能优化目标来权衡。在性能分析方面,无论是 Java 还是 Scala,都需要...
闭包是一种函数式编程特性,允许函数保留对外部环境的引用,即使该函数被作为独立实体传递或执行。在Java中,接口中的默认方法和Lambda表达式是实现闭包的关键。 在Java 8中,引入了Lambda表达式来简化匿名内部类的...
通过实验,我不仅掌握了函数式编程的基本概念,如纯函数、柯里化、闭包等,还学会了如何利用这些概念解决实际问题。在面对复杂问题时,函数式编程的思维方式帮助我找到了更优雅的解决方案。 总之,函数式编程以其...
闭包(closure)是函数式编程中的概念,出现于 20 世纪 60 年代,最早实现闭包的语言是 Scheme,它是 LISP 的一种方言。之后闭包特性被其他语言广泛吸纳。 闭包的严格定义是“由函数(环境)及其封闭的自由变量组成...
Groovy是一种强大的、动态的、基于Java平台的脚本语言,它吸收了多种编程范式的优点,其中包括函数式编程。函数式编程是一种编程范式,它强调数据处理和计算作为数学函数,避免改变状态和可变数据。在Groovy中,我们...
三、Java函数式编程实践 1. **集合操作**:使用Lambda表达式和Stream API,可以对集合进行高效且易于理解的操作,如查找、过滤、排序、映射和减少。 2. **并行处理**:Stream API支持并行流,利用多核处理器提高...
Lambda 表达式简化了匿名内部类的使用,使得代码更易读、易维护,而默认方法则增强了接口的功能性,促进了函数式编程风格在 Java 中的应用。了解并熟练运用这些特性,对于提升 Java 编程效率和代码质量具有重要意义...
这本书专门针对Java开发者,介绍了函数式编程(Functional Programming,简称FP)的基本概念和原则,以及它与Java语言结合的实践应用。 在Java开发者的日常工作中,他们可能面临着并发编程的挑战、数据管理问题、...
在深入学习Java内存化和函数式协同的主题中,我们将探讨两个关键概念:内存化和函数式编程中的协同。首先,让我们详细理解内存化的概念及其在Java中的应用。 内存化是一种优化技术,它允许函数在计算过某组参数的值...
1. Scala语言特性:Scala是一种针对Java虚拟机(JVM)的编程语言,它将面向对象和函数式编程的特性无缝融合,旨在提供一种简洁、优雅且类型安全的方式来表达常见的编程模式。其目标是提高Java程序员的编程效率。 2....