我最近阅读了很多关于 Java 的下一个版本可能支持闭包的信息。我觉得我对什么是闭包有一个非常牢固的把握,但是我想不出一个可靠的例子来说明它们如何使面向对象的语言“更好”。谁能给我一个需要(甚至首选)关闭的特定用例?
19 回答
作为一名 Lisp 程序员,我希望 Java 社区能够理解以下区别:作为对象的函数与闭包。
a)函数可以命名或匿名。但它们也可以是它们自己的对象。这允许函数作为参数传递、从函数返回或存储在数据结构中。这意味着函数是编程语言中的第一类对象。
匿名函数并没有给语言增加太多,它们只是允许你以更短的方式编写函数。
b)闭包是一个函数加上一个绑定环境。闭包可以向下传递(作为参数)或向上返回(作为返回值)。这允许函数引用其环境的变量,即使周围的代码不再处于活动状态。
如果你有某种语言的 a),那么问题就来了b)怎么办?有些语言有a),但没有b)。在函数式编程世界中,a)(函数)和b(作为闭包的函数)是当今的规范。Smalltalk 长期以来一直有a)(块是匿名函数),但后来 Smalltalk 的一些方言增加了对b)(作为闭包的块)的支持。
你可以想象,如果你在语言中添加函数和闭包,你会得到一个稍微不同的编程模型。
从实用的角度来看,匿名函数添加了一些简短的符号,您可以在其中传递或调用函数。这可能是件好事。
闭包(函数加绑定)允许您创建一个可以访问某些变量(例如计数器值)的函数。现在您可以将该函数存储在一个对象中,访问它并调用它。函数对象的上下文现在不仅是它可以访问的对象,而且是它可以通过绑定访问的变量。这也很有用,但是您可以看到变量绑定与对对象变量的访问现在是一个问题:什么时候应该是词法变量(可以在闭包中访问),什么时候应该是某个对象的变量(一个插槽)。什么时候应该是闭包或对象?您可以以类似的方式使用两者。学习 Scheme(一种 Lisp 方言)的学生通常的编程练习是使用闭包编写一个简单的对象系统。
结果是更复杂的编程语言和更复杂的运行时模型。太复杂?
它们并没有使面向对象的语言变得更好。它们使实用语言更加实用。
如果您正在使用 OO 锤子解决问题 - 将所有内容都表示为对象之间的交互 - 那么闭包就没有意义了。在基于类的 OO 语言中,闭包是烟雾缭绕的密室,事情已经完成,但事后没有人谈论它。从概念上讲,这是可憎的。
在实践中,它非常方便。我真的不想定义一种新类型的对象来保存上下文,为它建立“do stuff”方法,实例化它并填充上下文......我只想告诉编译器,“看,看看我现在可以访问吗?这就是我想要的上下文,这是我想要使用它的代码 - 为我保留这个,直到我需要它为止。
很棒的东西。
最明显的事情是对所有那些只有一个名为 run() 或 actionPerformed() 或类似方法的类的伪替换。因此,与其创建嵌入了 Runnable 的线程,不如使用闭包。没有比我们现在拥有的更强大,但更方便和简洁。
那么我们需要闭包吗?不,他们会很高兴吗?当然,只要他们不像我担心的那样被束缚住了。
我想为了支持核心函数式编程概念,你需要闭包。通过对闭包的支持使代码更加优雅和可组合。另外,我喜欢将代码行作为参数传递给函数的想法。
有一些非常有用的“高阶函数”可以使用闭包对列表进行操作。高阶函数是具有“函数对象”作为参数的函数。
例如,对列表中的每个元素应用一些转换是一种非常常见的操作。这种高阶函数通常称为“map”或“collect”。(参见Groovy的 *.spread 运算符)。
例如,在没有闭包的情况下对列表中的每个元素进行平方,您可能会这样写:
List<Integer> squareInts(List<Integer> is){
List<Integer> result = new ArrayList<Integer>(is.size());
for (Integer i:is)
result.add(i*i);
return result;
}
使用闭包和映射以及建议的语法,你可以这样写:
is.map({Integer i => i*i})
(关于原始类型的装箱,这里可能存在性能问题。)
正如 Pop Catalin 所解释的,还有另一个称为“选择”或“过滤器”的高阶函数:它可用于获取列表中符合某些条件的所有元素。例如:
代替:
void onlyStringsWithMoreThan4Chars(List<String> strings){
List<String> result = new ArrayList<String>(str.size()); // should be enough
for (String str:strings)
if (str.length() > 4) result.add(str);
return result;
}
相反,你可以写类似
strings.select({String str => str.length() > 4});
使用该提案。
您可能会查看 Groovy 语法,它是 Java 语言的扩展,现在支持闭包。有关如何处理闭包的更多示例,请参阅Groovy 用户指南的集合章节。
备注:
可能需要对“关闭”一词进行一些澄清。我上面显示的严格来说是没有闭包的。它们只是“功能对象”。闭包是可以捕获或“关闭”围绕它的代码的(词法)上下文的所有内容。从这个意义上说,Java 现在有闭包,即匿名类:
Runnable createStringPrintingRunnable(final String str){
return new Runnable(){
public void run(){
System.out.println(str); // this accesses a variable from an outer scope
}
};
}
Java 不需要闭包,面向对象的语言可以完成闭包所做的一切,使用中间对象存储状态或执行操作(在 Java 的情况下为内部类)。但是闭包作为一种特性是可取的,因为它们极大地简化了代码并提高了可读性,从而提高了代码的可维护性。
我不是 Java 专家,但我使用的是 C# 3.5,而闭包是我最喜欢的语言特性之一,例如以下面的语句为例:
// Example #1 with closures
public IList<Customer> GetFilteredCustomerList(string filter) {
//Here a closure is created around the filter parameter
return Customers.Where( c => c.Name.Contains(filter)).ToList();
}
现在举一个不使用闭包的等效示例
//Example #2 without closures, using just basic OO techniques
public IList<Customer> GetFilteredCustomerList(string filter) {
return new Customers.Where( new CustomerNameFiltrator(filter));
}
...
public class CustomerNameFiltrator : IFilter<Customer> {
private string _filter;
public CustomerNameFiltrator(string filter) {
_filter = filter;
}
public bool Filter(Customer customer) {
return customer.Name.Contains( _filter);
}
}
我知道这是 C# 而不是 Java,但想法是一样的,闭包有助于简洁,并使代码更短且更具可读性。在幕后,C# 3.5 的闭包做了一些看起来与示例 #2 非常相似的事情,这意味着编译器在幕后创建了一个私有类并将“过滤器”参数传递给它。
Java 不需要闭包来工作,作为开发人员,您也不需要它们,但是,它们很有用并提供好处,这意味着它们在生产语言中是可取的,其目标之一是生产力.
我最近一直在阅读很多关于 Java 的下一个版本可能支持闭包的信息。我觉得我对什么是闭包有相当的把握,但我想不出一个可靠的例子来说明它们如何使面向对象的语言“更好”。
好吧,大多数使用“闭包”一词的人实际上是指“函数对象”,从这个意义上说,函数对象可以在某些情况下编写更简单的代码,例如在排序函数中需要自定义比较器时。
例如,在 Python 中:
def reversecmp(x, y):
return y - x
a = [4, 2, 5, 9, 11]
a.sort(cmp=reversecmp)
这通过传递自定义比较函数 reversecmp 以相反的顺序对列表 a 进行排序。添加 lambda 运算符使事情变得更加紧凑:
a = [4, 2, 5, 9, 11]
a.sort(cmp=lambda x, y : y - x)
Java 没有函数对象,所以它使用“函子类”来模拟它们。在 Java 中,您通过实现 Comparator 类的自定义版本并将其传递给 sort 函数来执行等效操作:
class ReverseComparator implements Comparator {
public compare(Object x, Object y) {
return (Integer) y - (Integer) x;
}
...
List<Integer> a = Arrays.asList(4, 2, 5, 9, 11);
Collections.sort(a, new ReverseComparator());
如您所见,它提供与闭包相同的效果,但更笨拙且更冗长。然而,匿名内部类的添加消除了大部分痛苦:
List<Integer> a = Arrays.asList(4, 2, 5, 9, 11);
Comparator reverse = new Comparator() {
public Compare(Object x, Object y) {
return (Integer) y - (Integer) x;
}
}
Collections.sort(a, reverse);
所以我会说Java中函子类+匿名内部类的组合足以弥补真正的函数对象的不足,使它们的添加变得不必要。
Java 从 1.1 开始就有闭包,只是以一种非常麻烦和有限的方式。
只要您有一些描述的回调,它们通常很有用。一个常见的情况是抽象出控制流,留下有趣的代码来调用一个没有外部控制流的闭包的算法。
一个简单的例子是 for-each(尽管 Java 1.5 已经有了)。虽然您可以按原样在 Java 中实现 forEach 方法,但它过于冗长而无用。
一个已经对现有 Java 有意义的示例是实现“围绕执行”的习惯用法,从而抽象了资源的获取和释放。例如,文件打开和关闭可以在 try/finally 中完成,而无需客户端代码正确获取详细信息。
一些人已经说过或暗示,闭包只是语法糖——做你已经可以用匿名内部类做的事情,并使它更方便地传递参数。
它们是语法糖,就像 Java 是汇编器的语法糖一样(为了论证,“汇编器”可以是字节码)。换句话说,他们提高了抽象级别,这是一个重要的概念。
闭包将函数作为对象的概念提升为第一类实体——增加了代码的表达能力,而不是用更多的样板来使代码混乱。
Tom Hawtin 已经提到了一个贴近我心的例子——实现 Execute Around 习惯用法,这几乎是将 RAII 引入 Java 的唯一方法。几年前,当我第一次听说可能会关闭时,我写了一篇关于这个主题的博客文章。
具有讽刺意味的是,我认为闭包对 Java 有利的原因(更多的表达力和更少的代码)可能是让许多 Java 拥护者感到不安的原因。Java 有一种“把所有东西都拼出来”的心态。闭包是对更实用的做事方式的认可——我也认为这是一件好事,但可能会淡化 Java 社区中许多人所珍视的纯 OO 信息。
当闭包最终出现在 Java 中时,我会兴高采烈地摆脱我所有的自定义比较器类。
myArray.sort( (a, b) => a.myProperty().compareTo(b.myProperty() );
......看起来比......好多了
myArray.sort(new Comparator<MyClass>() {
public int compare(MyClass a, MyClass b) {
return a.myProperty().compareTo(b.myProperty();
}
});
现在JDK8即将发布,有更多可用信息可以丰富这个问题的答案。
Oracle 的语言架构师 Bria Goetz 发表了一系列关于 Java 中 lambda 表达式当前状态的论文(尚未草稿)。它还涵盖了闭包,因为他们计划在即将发布的 JDK 中发布它们,该 JDK 应该在 2013 年 1 月左右完成代码,并应该在 2013 年年中左右发布。
- Lambda 的状态:在本文的第一页或第二页中,试图回答这里提出的问题。虽然我仍然觉得它的论点很短,但它充满了例子。
- The State of Lambda - Libraries Edition:这也很有趣,因为它涵盖了惰性评估和并行性等优点。
- The Translation of Lambda Expressions:它基本上解释了 Java 编译器完成的脱糖过程。
在过去的几天里,我一直在思考这个非常有趣的问题的话题。首先,如果我理解正确的话,Java 已经有了一些基本的闭包概念(通过匿名类定义),但是要引入的新特性是对基于匿名函数的闭包的支持。
这个扩展肯定会让语言更具表现力,但我不确定它是否真的适合其他语言。Java 被设计为一种不支持函数式编程的面向对象语言:新语义是否易于理解?Java 6 甚至没有函数,Java 7 会有匿名函数但没有“普通”函数吗?
我的印象是,随着函数式编程等新的编程风格或范式变得越来越流行,每个人都希望在自己喜欢的 OOP 语言中使用它们。这是可以理解的:人们希望在采用新功能的同时继续使用他们熟悉的语言。但这样一来,语言就会变得非常复杂并失去连贯性。
所以我目前的态度是在面向对象编程方面坚持使用 Java 6(我希望 Java 6 仍然会被支持一段时间),如果我真的对做 OOP + FP 感兴趣,可以看看其他一些语言,比如Scala(Scala 从一开始就被定义为多范式,可以很好地与 Java 集成)而不是切换到 Java 7。
我认为 Java 的成功归功于它结合了一种简单的语言与非常强大的库和工具这一事实,而且我不认为像闭包这样的新特性会使它成为一种更好的编程语言。
命令式语言中的闭包(例如:JavaScript、C#、即将到来的 C++ 更新)与匿名内部类不同。他们需要能够捕获对局部变量的可修改引用。Java 的内部类只能捕获局部final
变量。
几乎任何语言功能都可以被批评为非必要的:
for
,while
,do
都只是goto
/的语法糖if
。- 内部类是具有指向外部类的字段的类的语法糖。
- 泛型是强制转换的语法糖。
完全相同的“非必要”论点应该阻止包含上述所有功能。
作为一名试图自学 lisp 以成为一名更好的程序员的 Java 开发人员,我想说我希望看到 Josh Block 提议的闭包实现。我发现自己使用匿名内部类来表达一些事情,比如在聚合一些数据时如何处理列表的每个元素。将其表示为一个闭包会很好,而不是必须创建一个抽象类。
不仅是本吉史密斯,我也喜欢你能做到的...
myArray.sort{ it.myProperty }
当属性的自然语言比较不适合您的需要时,您只需要显示的更详细的比较器。
我非常喜欢这个功能。
您可能想看看 Groovy,这是一种与 Java 最兼容的语言,并且在 JRE 上运行,但支持闭包。
可读性和可维护性怎么样...单行闭包更难理解和调试,imo 软件寿命很长,您可以让具有基本语言知识的人来维护它...所以比单行更好地展开逻辑为了便于维护...通常没有软件明星在软件发布后负责维护...
匿名函数中缺乏绑定[即,如果外部上下文的变量(以及方法参数,如果有一个封闭的方法)被声明为最终的,那么它们是可用的,但不是其他的),我不太明白这个限制实际上买了什么.
无论如何,我大量使用“最终”。所以,如果我的意图是在闭包内使用相同的对象,我确实会在封闭范围内声明这些对象最终。但是,让“闭包 [java aic]” 像通过构造函数传递一样获取引用的副本会有什么问题(事实上这就是它的完成方式)。
如果闭包想要覆盖引用,那就这样吧;它会在不更改封闭范围看到的副本的情况下这样做。
如果我们认为这会导致代码不可读(例如,在构造函数调用 aic 时可能无法直接查看对象引用是什么),那么至少让语法不那么冗长怎么样?斯卡拉?时髦?