【编者按】本文作者为专注于自然语言处理多年的 Pierre-Yves Saumont,Pierre-Yves 著有30多本主讲 Java 软件开发的书籍,自2008开始供职于 Alcatel-Lucent 公司,担任软件研发工程师。
本文主要介绍了 Java 8 中的闭包与局部套用功能,由国内 ITOM 管理平台 OneAPM编译呈现。
关于Java 8,存在着许多错误观念。譬如,认为Java 8给Java带来了闭包特性就是其中之一。这个想法是错的,因为闭包特性从Java诞生之初就已经存在了。然而闭包是有缺陷的。尽管Java 8似乎倾向于函数式编程,我们仍应尽力避免使用Java闭包。但是,Java 8并没有在此方面提供过多帮助。
我们知道,参数求值时间是使用方法和使用函数时的一个重大区别。在Java中,我们可以写一个带参数且有返回值的方法。但是,这可以被称作函数吗?当然不能。方法只可以通过调用进行操纵,这表示它的参数会在该方法执行前取值。这是Java中参数按值传递的结果。
函数则与之不同。操作函数时我们可以不计算参数,且对参数何时取值有绝对的控制权。而且,如果一个函数有多个参数,它们可以不同时取值。这一点通过局部套用就可以做到。但是首先,我们将考虑如何利用闭包进行实现。
闭包举例
对函数而言,闭包能够在封装的上下文中获取内容。在函数式编程中,一个函数的结果应当仅由其参数决定。很显然,闭包打破了这一准则。
请看Java 5/6/7中的示例:
private Integer b = 2; List list = Arrays.asList(1, 2, 3, 4, 5); System.out.println(calculate(list.stream(), 3).collect(toList())); private Stream calculate(Stream stream, Integer a) { return stream.map(new Function() { @Override public Integer apply(Integer t) { return t * a + b; } }); } public interface Function<T, U> { U apply(T t); }
以上代码将产生如下结果:
[5, 8, 11, 14, 17]
所得结果是函数 f(x) = x * 3 + 2 对于列 [1, 2, 3, 4, 5]
的映射。到这一步都没什么问题。但是3和2可以用其他值替换吗?换句话说,它难道不是函数f(x, a, b) = x * a + b 对于该列的映射吗?
是,也不是。不是的原因在于a和b都被隐性定义了final
关键词,因此它们在函数取值时作为常数参与计算。但是当然,它们的值也会有变动。它们的final
属性(在Java 8中隐性定义,在之前版本中则显性定义)只是编译器优化编译过程的一种方式。编译器并不在乎任何潜在的变动值。它只在乎引用有没有发生变动,也就是说,它想要确保Integer
整数对象a
和b
的引用不发生变化,但并不在意它们的取值。这个特性在以下代码中可以看出:
private Integer b = 2; private Integer getB() { return this.b; } List list = Arrays.asList(1, 2, 3, 4, 5); System.out.println(calculator.calculate(list.stream(), new Int(3)).collect(toList())); private Streamcalculate00(Streamstream, final Int a) { return stream.map(new Function() { @Override public Integer apply(Integer t) { return t * a.value + getB(); } }); } - static private class Int { public int value; public Int(int value) { this.value = value; } }
在这里,我们使用了可变对象a
(属于Int
类,而不是不可变的Integer
类),以及一个方法来获取b
。现在,我们来模拟一个有三个变量的函数,但是仍旧使用仅有一个变量的函数,同时使用闭包来代替其他两个变量。很显然,这是非函数性的,因为它打破了仅依赖于函数参数的准则。
结果之一是,尽管有需要,我们也不能在别的地方重用这个函数,因为它依赖于上下文而不仅仅依赖于参数。我们要复制这些代码才能实现重用。另一个结果是,由于它需要上下文才能运行,我们也不能单独进行函数测试。
那么,我们应该使用带有三个参数的函数吗?我们可能会认为,这不可能实现。因为具体的实现过程与三个参数何时取值相关。它们都在不同的地方取值。如果我们刚才使用的是带有三个参数的函数,它们就必须同时取值。而映射方法只会映射带一个参数的函数到流,不可能映射带有三个参数的函数。因此,其余两个参数在函数绑定时(也即传递给映射时)必须已经取值。解决方法是先对其余两个参数取值。
我们也可以用闭包来实现这一功能,但是所得代码是不可测试的,且可能存在重叠。
使用Java 8 的句法(lambdas)也无法改变这一状况:
private Integer b = 2; private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { return stream.map(t -> t * a + b); }
我们需要的是一种在不同时间获取三个参数的方法——Currying(局部套用,也称柯里化函数,尽管它其实是Moses Shönfinkel发明的)。
使用局部闭包
局部闭包就是逐一对函数参数取值,每一步都生成少一个参数的新函数。举例来看,如果我们有如下函数:
f(x, y, z) = x * y + z
我们可以同时取参数值为2,4,5,得到以下方程:
f(3, 4, 5) = 3 * 4 + 5 = 17
我们也可以只取一个参数为3,得到以下方程:
f(3, y, z) = g(y, z) = 3 * y + z
现在,我们得到了只有两个参数的新函数g。再对该函数进行局部套用,将4赋值给y:
g(4, z) = h(z) = 3 * 4 + z
给参数赋值的顺序对计算结果并无影响。此处,我们并不是在局部相加,(如果是局部相加,我们还得考虑运算符优先级。)而是在进行对函数的局部应用。
那么,我们如何在Java中实现这种方法呢?以下是在Java5/6/7中的应用:
private static List<Integer> calculate(List<Integer> list, Integer a) { return list.map(new Function<Integer, Function<Integer, Function<Integer, Integer>>>() { @Override public Function<Integer, Function<Integer, Integer>> apply(final Integer x) { return new Function<Integer, Function<Integer, Integer>>() { @Override public Function<Integer, Integer> apply(final Integer y) { return new Function<Integer, Integer>() { @Override public Integer apply(Integer t) { return x + y * t; } }; } }; } }.apply(b).apply(a)); }
以上代码完全可以实现所需功能,但是要想说服开发者,让他们用这种方式编写代码,恐怕非常困难!还好,Java 8的lambda句法提供了以下实现方式:
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { return stream.map(((Function<Integer, Function<Integer, Function<Integer, Integer>>>) x -> y -> t -> x + y * t).apply(b).apply(a)); }
怎么样?或者,是不是可以写得更简单一点:
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { return stream.map((x -> y -> t -> x + y * t).apply(b).apply(a)); }
完全可以,但是Java 8不能自行判断参数类型,因此我们必须使用manifest类型来帮助确认(manifest在Java规范中的意思是explicit)。为了让代码看起来更整洁,我们可以使用一些小技巧:
interface F3 extends Function<Integer, Function<Integer, Function<Integer, Integer>>> {} private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { return stream.map(((F3) x -> y -> z -> x + y * z).apply(b).apply(a)); }
现在,我们来为函数命名,并在必要时重用它:
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { F3 calculation = x -> y -> z -> x + y * z; return stream.map(calculation.apply(b).apply(a)); }
我们还可以声明计算函数为一个辅助类的静态成员,使用静态导入来进一步简化代码:
public class Functions { static Function<Integer, Function<Integer, Function<Integer, Integer>>> calculation = x -> y -> z -> x + y * z; } ... import static Functions.calculation; private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { return stream.map(calculation.apply(b).apply(a)); }
可惜,Java 8 鼓励的是使用闭包。不然,我会介绍更多能让局部套用的使用更为简便的功能性语法糖。比如,在Scala中,以上例子就可以这样改写:
stream.map(calculation(b)(a))
虽然在Java中我们没法这样写。可是,通过下面的静态方法,我们可以达到相似的效果:
static Function<Integer, Function<Integer, Function<Integer, Integer>>> calculation = x -> y -> z -> x + y * z; static Function<Integer, Integer> calculation(Integer x, Integer y) { return calculation.apply(x).apply(y); }
现在,我们可以写:
private Stream<Integer> calculate(Stream<Integer> stream, Integer a) { return stream.map(calculation(b, a)); }
请注意,calculation(b, a)
不是带有两个参数的函数。它只是一个方法,在将两个参数逐一地局部调用至一个带有三个参数的函数之后,它会返回一个带有一个参数的函数,该函数便可传递给映射函数。
现在,calculation
方法便可以单独测试了。
自动局部调用
在之前的例子中,我们已经亲手实践过局部调用了。然而,我们大可以编写程序来自动化调用过程。我们可以编写这样一个方法:它会接收带有两个参数的函数,并返回该函数的局部调用版本。写起来非常简单:
public <A, B, C> Function<A, Function<B, C>> curry(final BiFunction<A, B, C> f) { return (A a) -> (B b) -> f.apply(a, b); }
有必要的话,我们还可以写一个方法来颠倒这一过程。这个过程可以接受A的Function函数作为参数,返回一个可返回C的B的Function函数,最终返回一个返回C的A,B的BiFunction函数。
public <A, B, C> BiFunction<A, B, C> uncurry(Function<A, Function<B, C>> f) { return (A a, B b) -> f.apply(a).apply(b); }
局部调用的其他应用
局部调用的应用方式还有很多。最重要的应用是模拟多参数函数。在Java 8提供了单参数函数(java.util.functions.Function
)以及双参数函数(java.util.functions.BiFunction
)。但并未提供存在于其他语言中的三参数、四参数、五参数甚至更多参数的函数。其实,有没有这些函数并不重要。它们只是在特定情况下,需要同时对所有参数取值时应用的语法糖。实际上,这也是BiFunctin
在Java 8中存在的原因:函数的常见使用方法就是模拟二元运算符,(请注意:在Java 8中有BinaryOperator
接口,但它只用于两个参数以及返回值都属于同一类型的特殊情况。我们将在下一篇文章中讨论这一点。)
局部调用在函数的各个参数需要在不同地方取值时是非常好用的。通过局部调用,我们可以在某一组件中对一个参数取值,然后将计算结果传递到另一组件对其他参数取值,如此反复,直到所有参数值都被取到。
小结
Java 8并不是一种函数式语言(可能永远也不会是)。但是,我们仍可以在Java(甚至是Java 8之前的版本)中使用函数式范式。这样做的确会略有代价。但这种代价在Java 8中已经大幅减少了。尽管如此,想要写函数型代码的开发者还是得动动脑筋才能掌握这种范式。使用局部调用就是智力成果之一。
请记住:
(A, B, C) -> D
总是可以由如下方式替代:
A -> B -> C -> D
即便Java 8无法判断该表达方式的类型,你只要自行指定其类型就可以了。这就是局部调用,它总是比闭包更为稳妥。
OneAPM 能为您提供端到端的 Java 应用性能解决方案,我们支持所有常见的 Java 框架及应用服务器,助您快速发现系统瓶颈,定位异常根本原因。分钟级部署,即刻体验,Java 监控从来没有如此简单。想阅读更多技术文章,请访问 OneAPM 官方技术博客。
本文转自 OneAPM 官方博客
编译自:https://dzone.com/articles/whats-wrong-java-8-currying-vs
相关推荐
你并不了解 JavaScript(YDKJS)第二版:作用域与闭包
JSR-335 将闭包引入了 Java 。闭包在现在的很多流行的语言中都存在,例如 C++、C# 。闭包允许我 们创建函数指针,并把它们作为参数传递。在这篇文章中,将粗略的看一遍Java8的特性,并介绍 Lambda表达式。而且将试...
### Rust 编程艺术:迭代器与闭包的精妙运用 #### 一、Rust 语言概述 Rust 是一种高性能的系统级编程语言,它由 Mozilla 研究院发起,Graydon Hoare 设计,并于 2010 年首次发布。Rust 的设计目标在于提供内存安全...
第020讲:函数:内嵌函数和闭包 _ 课后测试题及答案,《零基础入门学习Python》,Python交流,鱼C论坛 - Powered by Discuz!.html
Java闭包是一个重要的编程概念,尤其在Java 8及以后的版本中得到了广泛的应用。闭包是一种函数式编程特性,允许函数保留对外部环境的引用,即使该函数被作为独立实体传递或执行。在Java中,接口中的默认方法和Lambda...
### 计算NFA中的ε闭包 #### 一、NFA与ε闭包概念介绍 **非确定有限自动机(NFA)**是一种理论计算模型,它扩展了确定有限自动机(DFA)的概念,允许在某些情况下从一个状态出发到达多个状态。在NFA中,存在一种特殊的...
闭包是JavaScript中的一个关键概念,对于理解函数的作用域、内存管理以及模块化设计至关重要。本项目“js-sandbox”旨在帮助开发者深入理解闭包,并通过实践加深对Web套接字(WebSocket)和函数式编程(FP)的理解。...
在Java 8中,递归和闭包是两个重要的概念,它们在函数式编程中扮演着核心角色。本文将深入探讨这两个概念,并结合实际示例解释它们如何在Java 8中协同工作。 首先,让我们理解“递归”。递归是一种算法,其中函数或...
在 Java 8 及以后的版本中,闭包的实现主要通过 Lambda 表达式和默认方法来体现。 Lambda 表达式是 Java 8 引入的一项重要特性,它提供了一种简洁的方式来表示没有名字的函数。Lambda 表达式的语法如下: `...
传递闭包是图论中的一个重要概念,特别是在研究有向图的可达性问题时。Warshall算法是由美国计算机科学家Stephen Warshall于1962年提出的,它主要用于计算有向图的所有节点对之间的传递闭包。在图中,如果存在一条从...
### 闭包权威指南 #### 一、书籍概述与作者介绍 《闭包:权威指南》(Closure: The Definitive Guide)由Michael Bolin撰写,于2010年由O'Reilly Media出版。本书是关于JavaScript闭包的深度解析与应用实践的权威...
It is important in distributed computer systems to identify those events (at identifiable points in time) that are concurrent, or not related to each other in time. A group of concurrent events may ...
8. **应用实例**:闭包运算在数据库查询(如关系代数中的闭包操作)、形式语言(如正则表达式闭包)、编译器设计(如控制流分析)以及网络路由(如可达性计算)等领域都有广泛应用。 理解并熟练掌握离散数学中的...
在iOS开发中,闭包(Closure)是一种强大的编程工具,它允许我们定义代码块并可以在程序的不同地方作为值传递。闭包可以捕获和存储它所在上下文中的常量和变量,这使得它们非常适合用于异步操作、回调函数或者封装一...
本文实例讲述了Python 闭包,函数分隔作用域,nonlocal声明非局部变量操作。分享给大家供大家参考,具体如下: 实例对象也可以实现闭包的功能,不过实例对象消耗的资源(内存)比闭包多。 demo.py(闭包): # 闭包,...
当一个函数返回后,通常它的局部变量会被销毁,但闭包却允许这些局部变量继续存在并被访问。 #### 示例分析 在提供的文件中,给出了一段示例代码: ```javascript function a() { var i = 0; function b() { ...
2. **变量持久化**:闭包可以保存外部函数的局部变量,即使外部函数已经执行完毕。这是因为闭包内部函数引用了这些变量,导致它们不会被垃圾回收器清理。 3. **内存管理**:闭包在内存管理上扮演着重要角色。由于它...
然而,对于某些社区,如Java,闭包的引入引发了一些争议。一些开发者认为闭包增加了语言的复杂性,破坏了简洁性,而另一些人则相信它们能带来新的设计模式和效率提升。 闭包的核心在于它可以捕获和存储其定义时的...
绘制抓取的三个维度。 用法:grabs(input1,input2, dist1, dist2) % 表示绘制把握基于四个论点。 参数:(输入) input1 - 用于通过 x_axis 进行 linspacing 的无符号整数。 input2 - 用于通过 y_axis 进行 ...
逃逸闭包必须满足下面2个条件: 1、闭包作为一个参数传到函数中 2、闭包在函数返回之后才执行 需要在参数前面加入标注: @escaping,用来指明这个闭包是允许“逃逸”出这个函数的。 注意:将一个闭包标记为 @...