Java 8怎么了:局部套用vs闭包
【编者按】本文作者为专注于自然语言处理多年的 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
Java 8怎么了:局部套用vs闭包的更多相关文章
- C#函数式程序设计之局部套用与部分应用
函数式设计的核心与函数的应用以及函数如何作为算法的基本模块有关.利用局部套用技术可以把所有函数看成是函数类的成员,这些函数只有一个形参,有了局部套用,才有部分应用.部分应用是使函数模块化成为可能的两个 ...
- Java基础-内部类-为什么局部和匿名内部类只能访问局部final变量
先看下面这段代码: public class Test { public static void main(String[] args) { } public void test(final int ...
- java程序员理解js中的闭包
1.闭包概念: 就是函数内部通过某种方式访问一个函数内部的局部变量 再次理解: 闭包产生原因: 1.内部函数引用了外部函数的变量 作用:延长局部变量的生命周期 让函数外部可以调用到函数内部的数据 利用 ...
- Java 内部类(成员内部类、局部内部类、静态内部类,匿名内部类)
一.什么是内部类? 内部类是指在一个外部类的内部再定义一个类.内部类作为外部类的一个成员,并且依附于外部类而存在的.内部类可为静态,可用protected和private修饰(而外部类只能使用publ ...
- JAVA学习Swing绝对局部简单学习
package com.swing; import java.awt.Container; import javax.swing.JButton; import javax.swing.JFrame; ...
- Java 基础 面向对象- 成员内部类/局部内部类/举例Comparable 接口的匿名内部类
笔记: package 任务135; /**类的 内部类, *1.相当于说, 我们可以在类的内部再定义类, * 2.成员内部类: * a.是外部类的一个成员,4个修饰符:static, final , ...
- Java内部类(3):局部内部类
有这样一种内部类,它是嵌套在方法和作用域内的,对于这个类的使用主要是应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成 ...
- java中什么是局部内部类Local inner class?
5.局部内部类Local inner class 马克-to-win:什么叫局部内部类?内部类声明位置:1.它的外部类的范围之内.2.在几个程序块的范围之内.例如,由方法定义的块中或甚至在for循环体 ...
- Java内部类之间的闭包和回调详解
前言 闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域.通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自 ...
随机推荐
- Moses 里的参数(未完成)
老师要求看看Moses里都有什么参数,调整了参数又会对翻译结果有什么影响,先将找到的参数列出来 首先是权重: [weight] WordPenalty0= LM= Distortion0= Phras ...
- XAML(4) - 标记扩展
在为元素设置值时, 可以直接设置值, 但有时标记扩展非常有帮助.标记扩展包含花括号,其后是定义了标记扩展类型的字符串标志. 下面是一个Static Resource标记扩展: <Button N ...
- sftp
SFTP 为 SSH的一部分,是一种传输档案至 Blogger 伺服器的安全方式.其实在SSH软件包中,已经包含了一个叫作SFTP(Secure File Transfer Protocol)的安全文 ...
- Excle中LOOKUP经典用法
在Excle中我们经常会遇到需要求根据某个区间的判断然后获取到对应的结果,下面是一个具体的实现例子: 例如: 现在需要实现,当输入0到25以内的任何数字时,会自动获取相应的英文字母 =IFERROR( ...
- MongoDB复制机制实例
MongoDB的主从复制是一个主可以多从已从又可以为主进行主从复制.在这里就是实现一主一从一个仲裁服务器使用一个数据库服务器通过提供不同的端口. 一.启动一个MongoDB服务名字是applicati ...
- arcgis中使用excel中x,y坐标创建点问题
文件——从x,y中添加,可以显示点的位置 右击图层导出数据时,出现无法绘制图形,生成shapefile文件的情况.经过排除数据发现 当x,y坐标值中出现null等异常值时,会出现上述无法导出的情况.
- 记录一下mvc发布
让别人也可以访问你电脑上的ASP.NET MVC创建的网站 http://www.cnblogs.com/laoqi/p/4169184.html
- nodejs base64 编码解码
普通字符串 编码解码: var b = new Buffer('JavaScript'); var s = b.toString('base64'); // SmF2YVNjcmlwdA== var ...
- 使用usb+preseed在virtualbox上安装ubuntu(一)
1.制作usb boot盘,在ubuntu上使用startup disk creater将ubuntu-server12.04.iso写入到usb中: 2.修改syslinux文件夹中的syslinu ...
- C#和asp.net执行外部EXE程序
这两天研究下.Net的执行外部EXE程序问题,就是在一个程序里通过按钮或其他操作运行起来另外一个程序,需要传入参数,如用户名.密码之类(实际上很类似单点登录,不过要简单的多的多):总结如下: 1.CS ...