`
yuping322
  • 浏览: 93191 次
  • 来自: ...
社区版块
存档分类
最新评论

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

阅读更多

提起向 Java™ 语言增加新的特性,每个人都有自己的一两个想法。随着 Java 平台的源代码日渐开放,而使用其他语言(例如 JavaScript 和 Ruby)作为服务器端应用程序日趋流行,因此关于 Java 语言未来的争论空前激烈。Java 语言是否应该包容像闭包这样的主流新特性,然而引入过多特性会不会使得这种好端端的语言过于庞杂?在这个月的 “ Java 理论与实践 ” 专题中,Brian Goetz 回顾了相关的概念,详细介绍了两种竞争的闭包方案。

在 跨越边界 系列最近的一篇文章中,我的朋友兼同事 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<Double> {
 public Double apply(Double x) {
 return x * x;
 }
 },
 new BinaryFunction<Double, Double> {
 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,throws E extends Exception>
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>(Double x) { return x*x; },
 BinaryFunction<Double, Double>(Double x, Double y) { return x+y; });
 
由于为传递给 mapReduce() 的函数所创建的对象是普通的匿名类实例,其函数体可以引用封闭域中定义的变量,清单 13 中的方法和清单 7 相比,唯一的区别在于语法的繁简程度。

结束语

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



分享到:
评论

相关推荐

    跨越边界:闭包

    闭包是一种强大的编程概念,它允许函数访问和操作其外部作用域中的变量,即使在函数执行完毕后仍然...因此,对于是否应该在编程语言中引入闭包,取决于开发者对复杂性与功能性的权衡,以及对语言设计理念的理解和偏好。

    Java SE 8的55个新特性

    Java SE 8,作为Java语言的一个重要版本,其推出的55个新特性极大地推动了Java语言的发展,并在软件开发领域产生了深远的影响。这些新特性不仅涉及语言层面的改动,还包括API的更新和扩展,以及对JDK开发工具的改进...

    浅析Java的发展现状与趋势.pdf

    Darcy已经确认语言的下一版本将增加某些“轻量级”的闭包。 (3) Sun宣布Java SE 5服务周期已经终结,J2SE 5.0 Update 22将是其最后一个更新版本。 (4) Java EE 6参考实现和GlassFish 3.0于2009年12月10日发布。 ...

    软件工程师笔记(c++,java等)

    例如,C++的模板元编程、Java的反射机制、JavaScript的闭包,以及JVM工作原理、HTTP协议、数据库优化等。 【JSP、Servlet、Struts、Hibernate知识点】 JSP(JavaServer Pages)是Java EE的一部分,用于生成动态...

    java模拟js高阶函数

    Java 模拟 JavaScript 的高阶函数是编程领域中一个有趣且实用的主题,它涉及到语言间的特性差异和跨语言编程的概念。在JavaScript中,高阶函数是指可以接受其他函数作为参数或者返回函数作为结果的函数。这在处理回...

    java_script

    JavaScript,通常简称为JS,是一种轻量级的解释性编程语言,主要应用于Web开发领域,用于增加网页的交互性和动态功能。它与Java虽然名字相似,但两者在本质上是完全不同的语言。JavaScript最初由Netscape公司的...

    Java script 权威指南

    JavaScript,又常被称为JS,是一种轻量级的解释型编程语言,主要应用于Web浏览器,用于增加网页的交互性、动态功能以及实现丰富的用户界面。"Java Script 权威指南"是一本深入探讨JavaScript语言的经典书籍,它覆盖...

    JAVA设计模式影印版.pdf

    文档中提到的系列书籍不仅覆盖了Java的基本结构和算法,还包括了面向对象设计模式、Java模式应用、J2EE核心模式以及Java在特殊领域的应用。这些书籍通过案例和实践,提供了从基础到高级的详细指导,适合不同层次的...

    基于Java的编译原理--LR(1)分析表构造(JAVA).zip

    在编程语言领域,编译原理是理解计算机程序如何被转换为机器可执行代码的关键理论基础。本资料包“基于Java的编译原理--LR(1)分析表构造(JAVA).zip”着重探讨了编译器设计的一个重要部分——LR(1)分析,这是一...

    此存储库包含Ruby、C、C++、Python和Java中问题的各种解决方案。_C++_Java_下载.zip

    _C++_Java_下载.zip"表明这是一个软件开发相关的资源集合,特别是针对五种编程语言:Ruby、C、C++、Python和Java。这个压缩包很可能是某个程序员或团队分享的代码示例、解决问题的算法或者学习资源。从文件名...

    java面试资料

    - **JavaScript**:前端编程语言,用于增加交互性。面试者需了解DOM操作、事件处理、AJAX、闭包、原型链等核心概念,并可能涉及jQuery、Vue.js、React.js等库或框架。 对于Java开发者来说,熟练掌握上述技术和框架...

    主流编程语言优劣考共13页.docx

    本文主要探讨了几种主流编程语言的优缺点,包括C、C++、Java和Ruby,以及它们各自支持的编程范式。 首先,我们来看C语言。C语言是一种强类型的静态编译语言,它强调过程式编程,即通过一系列函数调用来实现程序逻辑...

    主流编程语言-WLP.docx

    【Java语言】 Java是一种跨平台的、强类型的、面向对象的语言,特点包括: 1. **一次编写,到处运行**:Java的字节码可以在任何支持JVM的平台上运行。 2. **垃圾回收**:Java自动管理内存,避免了内存泄漏问题。 3...

    帮助文档(java,javascript,css)

    - 默认方法:在接口中添加有实现的方法,使得接口可以在不破坏向后兼容性的前提下增加新功能。 2. **Javascript参考手册.chm**:这是JavaScript的参考手册,涵盖了JavaScript的基本语法、对象、函数、DOM操作、...

    Java 面向对象和函数式编程的混合和scala的比较.pdf

    在编程世界中,Java 和 Scala 是两种非常重要的编程语言,它们各自有着独特的特性和优势。在面向对象编程(OOP)的基础上,Java 自 Java 8 开始引入了函数式编程的概念,而 Scala 从设计之初就同时支持这两种编程...

    JavaScript语言精髓与编程实践完整版611页

    3. **JavaScript中的函数式编程**:JavaScript支持函数式编程的许多特性,如高阶函数、闭包等,这些特性使得JavaScript成为一种非常适合进行函数式编程的语言。 #### 七、结论 通过上述分析,《JavaScript语言精髓...

    Java Script从入门到精通.zip

    "Java Script从入门到精通.zip"这个压缩包包含了丰富的学习资源,涵盖了JavaScript的基础知识到高级应用,帮助初学者逐步掌握这一语言。 首先,JavaScript与Java虽然名字相似,但两者并不相同。JavaScript是一种轻...

    java和C#和PHP和各种数据库优缺点.docx

    4. 可靠性和安全性:Java有严格的类型检查,不支持指针,自动垃圾回收机制,以及运行时的错误检查,保证了程序的可靠性和安全性。 5. 多线程:Java内置多线程支持,允许开发者轻松创建和管理多个并发执行的任务,...

Global site tag (gtag.js) - Google Analytics