Java 下一代: 没有继承性的扩展,第 3 部分

Groovy 元编程为您提供常见问题的简单解决方案

Java 下一代语言(Groovy、Scala 和 Clojure)以多种方式弥补了 Java™ 语言的扩展限制。本期 Java 下一代 文章将介绍通过 Groovy 的元编程工具可以提供的一些令人感到惊讶的扩展功能。

Neal Ford, Director / Software Architect / Meme Wrangler, ThoughtWorks

Photo of Neal FordNeal Ford is Director, Software Architect, and Meme Wrangler at ThoughtWorks, a global IT consultancy. He is also the designer and developer of applications, instructional materials, magazine articles, courseware, and video/DVD presentations, and he is the author or editor of books spanning a variety of technologies, including the most recent Presentation Patterns. He focuses on designing and building large-scale enterprise applications. He is also an internationally acclaimed speaker at developer conferences worldwide. Check out his website.



2013 年 10 月 24 日

关于本系列

Java 传承的是平台,而不是语言。有超过 200 种语言可以在 JVM 上运行,它们之中不可避免地会有一种语言最终取代 Java 语言,成为编写 JVM 程序的最佳方式。本系列将探讨 3 种下一代 JVM 语言:Groovy、Scala 和 Clojure,比较并对比新的功能和范例,让 Java 开发人员对自己近期的未来发展有大体的认识。

Java 下一代语言扩展现有的类和其他构件的方法有很多,前两期 Java 下一代 文章探讨了其中的一些方法。在本期文章中,我将继续该探索,仔细查看在多种上下文中实现扩展的 Groovy 元编程技术。

在 “没有继承性的扩展,第 1 部分” 中,在讨论使用类别类 ExpandoMetaClass 作为将新行为 “应用于” 现有类的机制时,我偶然接触了一些 Groovy 元编程特性。Groovy 中的元编程特性更深入一些:它们使得集成 Java 代码变得更容易,而且可以帮助您采用比 Java 语言更简洁的方式来执行常见任务。

接口强制转换(Interface coercion)

接口是 Java 语言中常​​见的语义重用机制。尝试以简洁的方式集成 Java 代码的其他语言应该提供简单的方法来具体化接口。在 Groovy 中,类可以通过传统的 Java 方式来扩展接口。但是,Groovy 还使得在方便时轻松地将闭包和映射强制转换成接口实例变得很容易。

单一方法强制转换

清单 1 中的 Java 代码示例使用 FilenameFilter 接口来定位文件:

清单 1. 在 Java 中使用 FilenameFilter 接口列出文件
import java.io.File;
import java.io.FilenameFilter;

public class ListDirectories {
    public String[] listDirectoryNames(String root) {
        return new File(root).list(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return new File(name).isDirectory();
            }
        });
    }
}

清单 1 中,我创建了一个新的匿名内部类,它覆盖了指定过滤条件的 accept() 方法。在 Groovy 中,我可以跳过创建一个新类的步骤,只将一个闭包强制转换成接口,如清单 2 所示:

清单 2. 在 Groovy 中通过使用闭包强制转换来模拟 FilenameFilter 接口
new File('.').list(
    { File dir, String name -> new File(name).isDirectory() }
     as FilenameFilter).each { println it }

清单 2 中,list() 方法想使用一个 FilenameFilter 实例作为参数。但我却创建了一个与接口的 accept() 签名相匹配的闭包,并在闭包的正文中实现接口的功能。在定义了闭包之后,我通过调用 as FilenameFilter 将闭包强制转换成适当的 FilenameFilter 实例。Groovy 的 as 运算符将闭包具体化为一个实现接口的类。该技术对于单一方法接口非常适用,因为方法和闭包之间存在一个自然映射。

对于指定多个方法的接口,被具体化的类为每个方法都调用了相同的闭包块。但只在极少数情况下,用相同代码来处理所有方法调用才是合理的。当您需要使用多个方法时,可以使用包含方法名称/闭包对的 Map,而不是使用单一的闭包。

映射

在 Groovy 中,还可以使用映射来表示接口。映射的键是代表方法名称的字符串,键值是实现方法行为的代码块。清单 3 中的示例将一个映射具体化为一个 Iterator 实例:

清单 3. 在 Groovy 中使用映射来具体化接口
h = [hasNext:{ h.i > 0 }, next:{h.i--}]
h.i = 10
def iterator = h as Iterator
                                                  
while (iterator.hasNext())
  print iterator.next() + ", "
// 10, 9, 8, 7, 6, 5, 4, 3, 2, 1,

清单 3 中,我创建了一个映射 (h),它包括 hasNextnext 键,以及它们各自的代码块。Groovy 假设映射键是字符串,所以我不需要用引号来包围该键。在每个代码块中,我用点符号 (h.i) 引用 h 映射的第三个键 (i)。这个点符号借鉴自人们所熟悉的对象语法,它是 Groovy 中的另一个语法糖示例。在使用 h 作为一个迭代器之前,不会执行代码块,我必须首先确保 i 有一个值,然后再使用 h 作为一个迭代器。我用 h.i = 10 设置 i 的值。然后,我将 h 选作一个 Iterator,并使用从 10 开始的整数集合。

通过使得映射能够动态地作为接口实例,Groovy 极大地减少了 Java 语言有时导致的一些语法问题。此特性很好地说明了 Java 下一代语言如何改进开发人员的体验。


ExpandoMetaClass

正如我在 "没有继承性的扩展,第 1 部分" 中所述,您可以使用 ExpandoMetaClass 将新方法添加到类 — 包括核心类,比如 ObjectStringExpandoMetaClass 对于其他一些用途也是有用的,比如将方法添加到对象实例,以及改善异常处理。

将方法添加到对象和从对象中删除方法

从将行为附加到类的那一刻起,使用 ExpandoMetaClass 对类执行的更改就会在全局生效。普遍性是这种方法的优势 — 这并不奇怪,因为这种扩张机制源自 Grails Web 框架(请参阅 参考资料)的创建。Grails 依赖于对核心类的全局变更。但有时您需要在不影响所有实例的情况下,采用有限的方式为一个类添加语义。对于这些情况,Groovy 提供了可以与对象的元类实例 交互的方式。例如,您可以将方法只添加到某个特定的对象实例,如清单 4 所示:

清单 4. 将行为附加到一个对象实例
def list = new ArrayList()
list.metaClass.randomize = { ->
    Collections.shuffle(delegate)
    delegate
}

list << 1 << 2 << 3 << 4
println list.randomize() // [2, 1, 4, 3]
println list             // [2, 1, 4, 3]

清单 4 中,我创建了 ArrayList 的一个实例 (list)。然后我访问了该实例以懒惰方式实例化的 metaClass 属性。我添加了一个方法 (randomize()),该方法返回执行 shuffle 之后的集合。在元类的方法声明中,delegate 代表对象实例。

不过,我的 randomize() 方法改变了底层集合,因为 shuffle() 是一个变异调用。在 清单 4 的第二行输出中,请注意,该集合被永久性地更改为新的随机顺序。令人高兴的是,通过解决这些问题,可以轻松地改变 Collections.shuffle() 等内置方法的默认行为。例如,清单 5 中的 random 属性是对 清单 4randomize() 方法的改进:

清单 5. 改进不良语义
def list2 = new ArrayList()
list2.metaClass.getRandom = { ->
  def l = new ArrayList(delegate)
  Collections.shuffle(l)
  l
}

list2 << 1 << 2 << 3 << 4
println list2.random // [4, 1, 3, 2]
println list2        // [1, 2, 3, 4]

清单 5 中,我让 getRandom() 方法的正文先复制列表,然后再改变它,这样就可以让原始列表保持不变。通过使用 Groovy 的命名约定,将属性自动映射到 getset 方法,我让 random 也成为一个属性,而不是一个方法。

使用属性技术来减少额外的括号干扰,导致了最近在 Groovy 中将方法链接在一起的方式的改变。该语言的版本 1.8 引入了命令链 的概念,支持创建更流畅的域特定语言(DSL)。DSL 通常扩充现有的类或对象实例来添加特殊的行为。

混合

Ruby 和类似语言中的一个流行特性是混合。混合让您能够不使用继承,而是将新的方法和字段添加到现有的层次结构中。Groovy 支持混合特性,如清单 6 所示:

清单 6. 使用混合特性来附加行为
class ListUtils {
  static randomize(List list) {
    def l = new ArrayList(delegate)
    Collections.shuffle(l)
    l
  }
}
List.metaClass.mixin ListUtils

清单 6 中,我创建了一个辅助类 (ListUtils) 并为其添加了一个 randomize() 方法。在最后一行中,我将 ListUtils 类与 java.util.List 混合在一起,让我的 randomize() 方法对 java.util.List 可用。也可以在对象实例中使用 mixin。这种技术通过将变更限制到某个单独的代码构件来帮助执行调试和跟踪,所以,对于将行为附加到类而言,这是最好的方式。

结合扩展点

Groovy 的元编程特性不仅在单独使用时非常强大,结合起来使用也非常有效。在动态语言中的一个常见细节是方法缺失(method missing) 钩 — 一个类能够以可控的方式响应尚未定义的方法,而不是抛出异常。如果出现未知的方法调用,Groovy 会在一个包含 methodMissing() 的类上调用该方法。您可以在通过 ExpandoMetaClass 增加的附加物中包含 methodMissing()。通过结合使用 methodMissing()ExpandoMetaClass,您可以使得 Logger 等现有的类更加灵活。清单 7 显示了一个示例:

清单 7. 混合 ExpandoMetaClassmethodMissing
import java.util.logging.*

Logger.metaClass.methodMissing = { String name, args ->
    println "inside methodMissing with $name"
    int val = Level.WARNING.intValue() +
        (Level.SEVERE.intValue() - Level.WARNING.intValue()) * Math.random()
    def level = new CustomLevel(name.toUpperCase(),val)
    def impl = { Object... varArgs ->
        delegate.log(level,varArgs[0])
    }
    Logger.metaClass."$name" = impl
    impl args
}

Logger log = Logger.getLogger(this.class.name)
log.neal "really messed this up"
log.minor_mistake "can fix later"

清单 7 中,我使用 ExpandoMetaClass 将一个 methodMissing() 方法附加到 Logger 类。现在,无论此 Logger 类在范围中的哪个位置,我在以后的代码中都可以通过有创意的方法调用日志,如 清单 7 中最后三行所示。


面向方面的编程

面向方面的编程(AOP)是一种流行的、实用的方法,可以超越 Java 技术的原有设计对其进行扩展。通过操纵字节码的编译过程,方面可以将新的代码 “编织” 到现有方法中。AOP 定义了一些术语,包括 切入点(pointcut),这是执行补充的位置。例如, 切入点是指在方法调用前添加的代码。

因为 Groovy 编译生成了 Java 字节码,所以在 Groovy 中也支持 AOP。但通过元编程可以在 Groovy 中复制 AOP,而且没有 Java 语言所要求的繁琐过程。 ExpandoMetaClass 使您能够访问一个方法,这样就无需引用该方法。之后,您可以重新定义该方法,也可以仍然调用方法的原始版本。AOP 的这种 ExpandoMetaClass 用法如清单 8 所示:

清单 8. 对 ExpandoMetaClass 使用面向方面的切入点
class Bank {
  def transfer(Account to, Account from, BigDecimal amount) {
    from.balance -= amount
    to.balance += amount
  }
}

class Account {
  def name, balance;

  @Override
  public String toString() {
    "Account{name:${name}, balance:${balance}}"
  }
}

def oldTransfer = 
  Bank.metaClass.getMetaMethod("transfer", [Account, Account, BigDecimal] as Object[])

Bank.metaClass.transfer = { Account to, Account from, BigDecimal amount ->
  println "Logging transfer: to:${to}, from:${from}, amount:${amount}"
  oldTransfer.invoke(delegate, [to, from, amount] as Object[])
}

def bank = new Bank()
def acctA = new Account(name:"A", balance:100.00)
def acctB = new Account(name:"B", balance:200.00)
println("Balances:A = ${acctA.balance}, B = ${acctB.balance}")
bank.transfer(acctA, acctB, 10.00)
println("Balances:A = ${acctA.balance}, B = ${acctB.balance}")
//Balances:A = 100.00, B = 200.00
//Logging transfer: to:Account{name:A, balance:100.00},
//    from:Account{name:B, balance:200.00}, amount:10.00
//Balances:A = 110.00, B = 190.00

清单 8,我创建了一个典型的 Bank 类,它只有一个 transfer() 方法。辅助的 Account 类包含简单的帐户信息。ExpandoMetaClass 包含一个 getMetaMethod()方法,用于检索对某个方法的引用。我使用了 清单 8 中的 getMetaMethod(),检索对现有 transfer() 方法的引用。然后,通过使用 ExpandoMetaClass,我创建了一个新的 transfer() 方法来取代旧的方法。在新方法的主体内,在写完日志语句后,我调用了原来的方法。

清单 8 包含一个前切入点 示例:我执行了 “额外的” 代码,然后再调用原来的方法。这在 Ruby 等动态语言中是一种常见的技术,其社区将该技术称为 Monkey Patching。(原来使用的术语是 Guerilla Patching,但它被错听为 Gorilla Patching,然后被更名为 Monkey Patching,就像是一种文字游戏。)其结果与 AOP 一样,但在 Groovy 中的动态扩展使您能够在语言本身内执行这个增强。


AST 转换

虽然 ExpandoMetaClass 及其相关特性如此强大,它们也不能覆盖所有扩展点。最终,最强大的元编程能够修改编译器的 Abstract Syntax Tree(AST,抽象语法树) — 由编译进程维护的内部数据结构。注释是其中一种挂钩位置,在这里可以插入转换操作。Groovy 预定义了一些有用的语言扩展,比如 AST 转换。

例如,@Lazy 注释(比如 @Lazy pets = ['Cat', 'Dog', 'Bird'])将数据结构的实例化推迟到必须评估它们的时候。Groovy 1.8 引入了一系列有用的结构性注释,其中一些会出现在清单 9 中:

清单 9. 在 Groovy 中有用的结构性注释
import groovy.transform.*;

@Immutable
@TupleConstructor
@EqualsAndHashCode
@ToString(includeNames = true, includeFields=true)
final class Point {
  int x
  int y
}

清单 9 中,Groovy 运行时会自动执行以下操作:

  • 生成元组风格构造函数
  • 生成 equals()hashCode() 方法
  • 使 Point 类不可变
  • 生成一个 toString() 方法

使用 AST 变换远远优于使用 I​​DE 或反射来生成基础架构方法。在使用 IDE 的时候,如果发生更改,则必须始终牢记重新生成一些方法。而反射比在编译​​时发生的代码生成更慢。

除了使用丰富的预定义 AST 转换之外,还可以使用 Groovy 提供的一个完整 API 来构建自己的 AST 转换。通过这个 API,可以访问最细粒度的底层抽象,从而改变生成代码的方式。


结束语

在本期文章中,您了解到 Groovy 通过其元编程特性提供的一系列令人眼花缭乱的扩展选项。在下一期 Java 下一代 文章中,我会探索特征(混合功能)和 Scala 中的其他元编程。

参考资料

学习

  • The Productive Programmer(Neal Ford,O'Reilly Media,2008 年):Neal Ford 的书展开描述了本系列中的一些主题。
  • Clojure:Clojure 是在 JVM 上运行的一种现代的函数式语言。
  • Scala:Scala 是 JVM 上一种现代的函数式语言。
  • Groovy:Groovy 是 Java 的一个动态变体,拥有更新的语法和功能。
  • Grails:Grails 是使用 Groovy 编写的一个流行的 Web 框架。
  • 实战 Groovy(Andrew Glover 和 Scott Davis,developerWorks,2004-2009):在此 developerWorks 系列中探索 Groovy 的实际应用,并学习何时以及如何成功地应用它们。
  • 精通 Grails(Scott Davis,developerWorks,2008-2009):在此系列文章中学了解 Grails。
  • Advanced Groovy Tips and Tricks:感谢 Ken Kousen 提供了本文中的一些 Groovy 示例,它们取自此优秀的演示文稿。
  • 函数式思维:在 developerWorks 上 Neal Ford 的专栏系列中探索函数式编程。
  • 该作者的更多文章(Neal Ford,developerWorks,2005 年 6 月至今):了解 Groovy、Scala、Clojure、函数式编程、架构、设计、Ruby、Eclipse 和其他 Java 相关技术。
  • developerWorks Java 技术专区:查找数百篇关于 Java 编程方方面面的文章。

获得产品和技术

  • 下载 评估版本的 IBM 产品,并动手试用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。

讨论

  • 加入 developerWorks 社区。探索由开发人员推动的 博客、论坛、组和维基,并与其他 developerWorks 用户进行交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Open source
ArticleID=949215
ArticleTitle=Java 下一代: 没有继承性的扩展,第 3 部分
publish-date=10242013