java 匿名函数_Java 理论与实践,闭包之争

Java 语言是否应增加闭包以及如何添加?

跨越边界 系列最近的一篇文章中,我的朋友兼同事 Bruce Tate 以 Ruby 为例描述了闭包的强大功能。最近在安特卫普召开的 JavaPolis 会议上,听众人数最多的演讲是 Neal Gafter 的 “向 Java 语言增加闭包特性”。在 JavaPolis 的公告栏上,与会者可以写下和 Java 技术有关(或者无关)的想法,其中将近一半和关于闭包的争论有关。最近似乎 Java 社区的每个人都在讨论闭包——虽然闭包这一业已成熟的概念早在 Java 语言出现的 20 年之前就已经存在了。

本文中,我的目标是介绍关于 Java 语言闭包特性的种种观点。本文首先介绍闭包的概念及其应用,然后简要说明目前提出来的相互竞争的一些方案。

闭包:基本概念

闭包是可以包含自由(未绑定)变量的代码块;这些变量不是在这个代码块或者任何全局上下文中定义的,而是在定义代码块的环境中定义。“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量的存在,相关变量引用没有释放)和为自由变量提供绑定的计算环境(作用域)。在 Scheme、Common Lisp、Smalltalk、Groovy、JavaScript、Ruby 和 Python 等语言中都能找到对闭包不同程度的支持。

闭包的价值在于可以作为函数对象 或者匿名函数,对于类型系统而言这就意味着不仅要表示数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到变量中、作为参数传递给其他函数,最重要的是能够被函数动态地创建和返回。比如下面清单 1 所示的 Scheme 例子(摘自 SICP 3.3.3):

清单 1. Scheme 编程语言的函数示例,该函数接受另一个函数作为参数并返回缓存后的函数

(define (memoize f) (let ((table (make-table))) (lambda (x) (let ((previously-computed-result (lookup x table))) (if (not (null? previously-computed-result)) previously-computed-result (let ((result (f x))) (insert! x result table) result))))))

上述代码定义了一个叫做 memoize 的函数,接受函数 f 作为其参数,返回和 f 计算结果相同的另一个函数,不过新函数将以前的计算结果保存在表中,这样读取结果更快。返回的函数使用 lambda 结构创建,该结构动态创建新的函数对象。斜体显示的标识符在新定义函数中是自由的,它们的值在创建该函数的环境中绑定。比如,用于存储缓存数据的表变量在调用 memoize 的时候创建,由于被新建的函数引用,因此直到垃圾回收器回收结果函数的时候才会被收回。如果调用结果函数时带有参数 x ,它首先检查是否已经计算过 f(x)。是的话返回已经得到的 f(x),否则计算 f(x) 并在返回之前保存到表中以备后用。

闭包为创建和操纵参数化的计算提供了一种紧凑、自然的方式。可以认为支持闭包就是提供将 “代码块” 作为第一级对象处理的能力:能够传递、调用和动态创建新的代码块。要完全支持闭包,这种语言必须支持在运行时操纵、调用和创建函数,还要支持函数可以捕获创建这些函数的环境。很多语言仅提供了这些特性的一个子集,具备闭包的部分但不是全部优势。关于是否要在 Java 语言中增加闭包,关键问题在于提高表达能力所带来的益处能否与更高的复杂性所带来的代价相抵消。

匿名类和函数指针

C 语言提供了函数指针,允许将函数作为参数传递给其他函数。但是,C 中的函数不能有自由变量:所有变量在编译时必须是已知的,这就降低了函数指针作为一种抽象机制的表达能力。

Java 语言提供了内部类,可以包含对封闭对象字段的引用。该特性比函数指针更强大,因为它允许内部类实例保持对创建它的环境的引用。乍看起来,内部类似乎确实提供了闭包的大部分作用,虽然这还不是全部作用。您可以很容易构造一个名为 UnaryFunction 的接口,并创建能够缓存任何 unary 函数的缓存包装程序。但是这种方法通常不易于实现,它要求与函数交互的所有代码在编写时都必须知道这个函数的 “框架”。

闭包作为一种模式模板

匿名类允许创建这样的对象,该对象能够捕获定义它们的一部分环境,但是对象和代码块不一样。以一个常见的编码模式为例,如执行带有 Lock 的代码块。如果需要递增带有 Lock 的计数器,代码如清单 2 所示——即使这么简单的操作也非常罗嗦:

清单 2. 执行加锁代码块的规范用法

lock.lock();try {  ++counter;}finally { lock.unlock();}

如果能够提取出加锁管理代码就好了,这样会使代码看起来更紧凑,也不容易出错。首先可以创建如清单 3 所示的 withLock() 方法:

清单 3. 提取了 “加锁执行” 的概念,但是问题在于缺乏异常的透明性

public static void withLock(Lock lock, Runnable r) { lock.lock(); try {  r.run(); } finally { lock.unlock(); }}

不幸的是,这种方法只能达到您预期的部分目标。创建这种抽象代码的目标之一是使代码更紧凑;但是,匿名内部类的语法不是很紧凑,调用代码看起来如清单 4 所示:

清单 4. 清单 3 中 withLock() 方法的客户端代码

withLock(lock,  new Runnable() { public void run() { ++counter; }});

要递增一个加锁的计数器仍然需要编写很多代码!另外,将受到锁保护的代码块转化成方法调用所带来的抽象问题大大增加了问题的复杂性——如果受保护的代码块抛出一个检测异常怎么办?现在我们不能使用 Runnable 来表示执行的任务,而必须创建一种新的表示方法以允许在方法调用中抛出异常。不幸的是,在这里泛化也帮不上多少忙,虽然方法可以用泛型参数 E, 表示可能抛出的检测异常,但是这种方法不能很好地泛化抛出多种检测异常类型的方法(这就是为何 Callable 中的 call() 方法声明为抛出 Exception 而不是用类型参数指定一个类型的原因)。清单 3 中的方法最大的问题在于缺乏异常透明性,除此之外,还存在其他非透明性的问题,在 清单 4 的 Runnable 上下文中,return 或 break 这类语句的含义,与 清单 2 中 try 语句块中的一般意义不同。

理想情况下,受保护的递增操作应该像清单 5 所示的那样,并且块中代码的含义和 清单 2 的扩展形式相同:

清单 5. 清单 3 客户端代码的理想形式(但是是假设形式)

withLock(lock,  { ++counter; });

在语言中添加闭包以后,就可以创建行为类似控制流结构的方法,比如 “加锁执行这段代码”、“操作流并在完成后将其关闭” 或者 “为代码块的执行计时” 等。这种策略有可能简化某些类型的代码,这些代码反复使用特定编码模式或者惯用法,比如 清单 2 所示的加锁用法。(在一定程度上提供类似表达能力的另一种技术是 C 预处理器,它可以将 withLock() 操作用预处理宏表示,虽然和闭包相比宏更难以组织,而且安全性也更差。)

泛化算法的闭包

闭包能够大大简化代码的另一个地方是泛化算法的使用。随着多处理器计算机越来越便宜,利用小粒度并行机制的重要性日渐突出。使用泛化算法定义计算为库实现在问题空间中采用并行机制提供了一种自然的方式。

比方说,假设要计算一个大型数字集合的平方和。清单 6 给出了一种计算方法,但这种方法是按顺序计算结果的,对于大规模多处理器系统可能不是效率最高的方法:

清单 6. 顺序计算平方和

double sum;for (Double d : myBigCollection) sum += d*d;

每次循环迭代有两个操作:取平方,累加到最终结果。平方操作是互相独立的,可以并行执行;加法操作也不一定要执行 N 次,如果计算组织得当,只要 log(N) 次操作即可完成。

清单 6 中的操作是 map-reduce 算法的一个示例,对大批数据元素中的每一个数据元素应用一个函数,然后将每次应用该函数计算出的结果通过某种累加函数累加起来。假设有一个 map-reduce 实现过程接受数据集作为输入,用一元函数处理每个元素,用二元函数累加结果,则可用清单 7 所示的代码完成平方和运算:

清单 7. 使用 MapReduce 计算平方和,可以实现并行执行

Double sumOfSquares = mapReduce(myBigCollection, new UnaryFunction { public Double apply(Double x) { return x * x; } }, new BinaryFunction { public Double apply(Double x, Double y) { return x + y; } });

假设清单 7 中的 mapReduce() 实现知道哪些操作可以并行执行,因而可以将函数应用和累加过程并行执行,从而改进并行系统的吞吐量。但是清单 7 中的代码不简洁,用了更多代码来表达和清单 6 中三行代码等价的泛化算法。

通过闭包可以更好地管理清单 7 中的代码。比如,清单 8 中的闭包语法和目前提出的 Java 语言闭包方案都不一样,目的仅在于说明闭包对泛化算法的支持:

清单 8. 使用 MapReduce 和假设的闭包语法计算平方和

sumOfSquares = mapReduce(myBigCollection,  function(x) {x * x},  function(x, y) {x + y});

清单 8 中基于闭包的算法具有两方面的好处:代码容易阅读和编写,抽象层次比顺序循环更高,能够有效地通过库实现并行。

闭包方案

目前至少提出了两种向 Java 语言增加闭包的方案。其一,绰号为 “BGGA”(名字源于其作者 Gilad Bracha、Neal Gafter、James Gosling 和 Peter von der Ahe),它扩展了类型系统,引入了 function 类型。其二,绰号为 “CICE” (代表 Concise Inner Class Expressions,简洁内部类表示),是由 Joshua Bloch、Doug Lea 和 “疯狂的” Bob Lee 所支持的,其目标更谦虚:简化匿名内部类实例的创建。 JSR 可能很快就会收到这方面的提议,考虑在未来的 Java 语言版本中支持闭包的形式和程度。

BGGA 方案

BGGA 方案提出了 function 类型的概念,即函数都带有一个类型参数列表、返回类型和 throws 子句。在 BGGA 方案中,计算平方和的代码将如清单 9 所示:

清单 9. 使用 BGGA 闭包语法计算平方和

sumOfSquares = mapReduce(myBigCollection,  { Double x => x * x },  { Double x, Double y => x + y });

=> 字符到左侧花括号之间的代码表示参数的名称和类型,右侧的代码表示定义的匿名函数的实现。这段代码可以引用块中定义的局部变量、闭包的参数以及创建闭包的作用域中的变量。

在 BGGA 方案中,可以声明 function 类型的变量、方法参数和方法返回值。在需要一个抽象方法类(如 Runnable 或 Callable)实例的任何上下文中都可以使用闭包,对于匿名类型的闭包,您可以使用带有给定参数列表的 invoke() 方法来调用。

BGGA 方案的主要目标之一是允许程序员创建行为类似控制结构的方法。因此,BGGA 还在语法上提出了一些吸引人的花招,允许像新的关键字那样调用接受闭包的方法,从而能够创建像 withLock() 或 forEach() 这样的方法,然后向控制原语一样调用它们。清单 10 说明了根据 BGGA 方案如何定义 withLock() 方法,清单 11 和 清单 12 说明了如何调用该方法,包括标准形式和“控制结构”形式:

清单 10. 采用 BGGA 闭包方案编写的 withLock() 方法

public static T withLock(Lock lock, {=>T throws E} block) throws E { lock.lock(); try { return block.invoke(); } finally { lock.unlock(); }}

清单 10 中的 withLock() 方法接受锁和闭包。闭包的返回类型和 throws 子句是泛化参数,编译器中的类型推断通常允许在未指定 T 和 E 值的情况下调用,如清单 11 和 12 所示:

清单 11. 调用 withLock()

withLock(lock, {=> System.out.println("hello");});

清单 12. 使用控制结构的缩写形式调用 withLock()

withLock(lock) { System.out.println("hello");}

和泛化一样,BGGA 方案中闭包的复杂性在很大程度上是由库的编写者来分担的,使用接受闭包的库方法更简单。

使用内部类实例是闭包所带来的好处,但是这种方法缺少透明性,BGGA 方案在一定程度上还有助于解决这个问题。比如,return、 break 和 this 在某一代码块中的语义与其在 Runnable(或其他内部类实例)中同一代码块中的语义是不同的。为了利用泛化算法而对代码进行移值的时候,这些不透明因素可能会造成混乱。

CICE 方案

CICE 方案要简单得多,它解决了实例化内部类实例不太灵活的问题。它没有建立函数类型的概念,只不过为一个抽象方法(如 Runnable、Callable 或 Comparator)内部类实例化提出了一种更紧凑的语法。

清单 13 说明了按照 CICE 如何计算平方和。它显示使用了 mapReduce() 中的 UnaryFunction 和 BinaryFunction 类型。mapReduce() 的参数是从 UnaryFunction 和 BinaryFunction 派生的匿名类,这种语法大大了降低了创建匿名实例的冗余。

清单 13. 采用 CICE 闭包方案计算平方和的代码

Double sumOfSquares = mapReduce(myBigCollection, UnaryFunction(Double x) { return x*x; }, BinaryFunction(Double x, Double y) { return x+y; });

由于为传递给 mapReduce() 的函数所创建的对象是普通的匿名类实例,其函数体可以引用封闭域中定义的变量,清单 13 中的方法和清单 7 相比,唯一的区别在于语法的繁简程度。

结束语

BGGA 方案为 Java 这种语言增加了功能强大的新武器,但是同时也为其语义和语法带来了可以预见的复杂性。另一方面,CICE 方案更简单:利用语言中已有的特性并使其更易于使用,但是没有增加重要的新功能。闭包是一种强大的抽象机制,用过之后多数人不愿意放弃。(问问那些熟悉 Scheme、Smalltalk 或 Ruby 编程的朋友对闭包的感想如何,他们可能会反问您对呼吸有什么感想。)但语言是有机的整体,为语言增加最初设计时没有预料到的新特性充满了危险,而且会增加语言的复杂性。争论的焦点不在于闭包是否有用——因为答案显然是肯定的——而在于为闭包重新改造 Java 语言的好处是否抵得上要付出的代价。

1b2339f49df1514601a9de7c9a007d95.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/507682.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

二维码识别中面临的主要问题

问题描述 下面有两个二维码的图片: 这两个二维码其实是一样的,也就是二维码码制在设计的时候,其实不需要那么大的黑点,只需要中心一小部分即可。 问题一:在优化二维码的过程中,能否考虑优化边缘部分&…

lia人是什么意思_69年属鸡人终身灾难是什么意思

69年属鸡人终身灾难是什么意思69年出生的生肖鸡,他们为人正直,有一颗转速飞快的脑袋,做事勤快,对生活非常乐观,但是因为过于自信总是做一些自己能力还不够的事,有句老话说的好“大难不死,必有后…

睡眠声音识别中的准确率问题(一)--问题描述及评价指标

1 问题描述 如何定义一个评价指标来评估睡眠声音识别中的准确率? 1.1 按照评分如何设计评价指标 睡眠声音识别预测的结果是一个概率分布,如下表所示: c1c_1c1​c2c_2c2​c3c_3c3​snortspeechcough0.50.20.70.550.450.77 在这种场景下&…

c51 嵌入汇编语言,在C51中嵌入汇编

在C51编程中,有时控制一些器件时,需要较强实时性,这时有必要在其中嵌入ASM语句,关于嵌入的方法众多,网上也可以找到相关说明,不过说得并不详细,这里结合本人调试18B20测温程序来说说如何在C中嵌…

睡眠声音识别中的准确率问题(二)--测试结果及分析

1 测试数据集介绍 (1)Audioset数据集: 从该数据集中筛选出鼾声数据500条; (2)人工采集的数据集:我们收集了鼾声及咳嗽数据50条,其中鼾声数据40条,咳嗽数据10条&#xff…

睡眠声音识别中的准确率问题(三)--采集的音频测试结果及分析

简介 本次我们对自己采集的93条Snoring音频进行测试并分析。 不区分音频质量的测试。 分别测试了NNN取1-5、8以及10时候的准确率,测试结果如下所示: N2345810Accuracy0.27960.47310.49460.49460.54840.5699 分析:从上表中可以得出…

中文 转gbk编码_go查找中文首字母

前言常见的通讯录都是按首字母来排序的,想象一下你要实现一个首字母排序的功能,若是全英文,那将会是一件很轻松的事情,一般语言层面就会提供排序方法,亦或自己实现也不是难事,那如果存在中文呢?…

语音增强相关技术综述

1 非监督语音增强技术 2 监督语音增强技术 3 github上提供的源代码及分析 3.1 Dual-signal Transformation LSTM Network 简介 https://github.com/breizhn/DTLN 用于实时噪声抑制的堆叠双信号变换 LSTM 网络 (DTLN) 的 Tensorflow 2.x 实现。这个存储库提供了在 python …

睡眠音频分割及识别问题(十二)--基于IOS的YAMNet音频识别(总结)

1 结构体 此部分定义了两个结构体用于存储音频信息: AudioFragment:用于存储单个音频片段(0.975s, 16000Hz)的标签评分信息;AudioInfo:用于存储整个音频信息,包含音频文件名、时间等基础信息&…

js 字符串替换_正则精要:玩转JS正则表达式,也许只需这一篇(建议收藏)

0.导引在正文开始前,先说说正则表达式是什么,为什么要用正则表达式?正则表达式在我个人看来就是一个程序可以识别的规则,有了这个规则,程序就可以帮我们判断某些字符是否符合我们的要求。但是,我们为什么要…

小米平板android最新版本,想要翻身还需努力 小米平板2安卓版评测

1依旧发烧?小米平板2评测如今的平板市场虽不能用日薄西山来形容,但各大厂商费尽心机惨淡经营也无力阻止平板电脑市场的衰落,iPad Air和Mini系列的销量下滑迫使苹果不得不用寄希望于iPad Pro打开一片新天地,而在手机行业风生水起了…

国内计算机类APP相关竞赛总结

中国高校计算机大赛–移动应用创新赛 http://www.appcontest.net/ 中国高校计算机大赛—移动应用创新赛”旨在促进高校计算机课程教学内容和教学方法改革,激发学生创新意识,提升学生利用计算机分析问题、解决问题的能力,特别是移动应用的设计…

flink 写kafka_flink消费kafka的offset与checkpoint

生产环境有个作业,逻辑很简单,读取kafka的数据,然后使用hive catalog,实时写入hbase,hive,redis。使用的flink版本为1.11.1。为了防止写入hive的文件数量过多,我设置了checkpoint为30分钟。env.…

论文阅读:超高分辨率图像中快速、准确的条码检测

摘要 由于目标对象的尺度不同,超高分辨率 (UHR) 图像中的对象检测长期以来一直是计算机视觉中的一个具有挑战性的问题。在条码检测方面,将 UHR 输入图像调整为更小的尺寸通常会导致相关信息的丢失,而直接处理它们的效率很高且计算成本很高。…

android 多线程 场景,精选Android初中级面试题 (三): 深探Handler,多线程,Bitmap

码个蛋(codeegg) 第 930 次推文作者:Focusing链接:https://juejin.im/post/5c85cead5188257c6703af47Handler1、谈谈消息机制Handler作用 ?有哪些要素 ?流程是怎样的 ?参考回答:负责跨线程通信,…

通过超分辨率重构来提高二维码的对比度

1 问题描述 (1)图像分辨率小。例如一些嵌入在海报(如图1)或远距离拍摄的码,其分辨率远小于通常情况下的码图像。 图1.海报中的二维码占比很小 (2)图像质量较低。有很多是经过了多次的压缩和转…

android web 访问数据库,Web下的JDBC访问数据库的基本步骤

Web下的JDBC访问数据库的基本步骤(2012-06-02 12:09:33)在Java程序中连接数据库的一般步骤分为一下几部分,我摘录出来,跟大家分享。(1)将数据库的JABC驱动加载到classpath中,在基于JavaEE的Web应用开发过程中,通常把JDBC驱动放在W…

linux 磁盘扩容_记录一次ESXi Linux在线扩容,不重启系统

因为工作需要,需要将运行在ESXi主机上面的一台Centos 里面的一个LV卷进行扩容,下面记录了此次扩展的详细过程,整个过程,不需要重启服务器。1. 首先通过df-h 查看当前磁盘结构如下:我们此次的最终目标,就是将…

android item三种,Android RecyclerView中的ItemDecoration的几种绘制方法

如题,我们使用recyclerview的时候,如果没有设置显示条目的margin,或者padding的话,是没有分割线效果的。那么除去使用margin或padding,其余的方法是用itemdecoration绘制分割线我们绘制分割线的时候通常会使用drawable去绘制&…

上传文件和提交textfield_0基础掌握Django框架(37)文件上传

为了更好的学习效果,请搭配视频教程一起学习:Django零基础到项目实战 - 网易云课堂​study.163.com文件上传:文件上传是网站开发中非常常见的功能。这里详细讲述如何在Django中实现文件的上传功能。前端HTML代码实现:在前端中&…