您所在的位置:主页 > JAVA技术 >

Java 下一代: 没有继承性的扩展

时间:2013-10-26 10:39来源:未知 作者:admin 点击:

      接口强制转换(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),它包括 hasNext 和

  next 键,以及它们各自的代码块。Groovy 假设映射键是字符串,所以我不需要用引号来包围该键。在每个代码块中,我用点符号

  (h.i) 引用 h 映射的第三个键 (i)。这个点符号借鉴自人们所熟悉的对象语法,它是 Groovy

  中的另一个语法糖示例。在使用 h 作为一个迭代器之前,不会执行代码块,我必须首先确保 i 有一个值,然后再使用

  h 作为一个迭代器。我用 h.i = 10 设置 i 的值。然后,我将 h

  选作一个 Iterator,并使用从 10 开始的整数集合。

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

  回页首

  ExpandoMetaClass

  正如我在 “没有继承性的扩展,第

  1 部分” 中所述,您可以使用 ExpandoMetaClass 将新方法添加到类 — 包括核心类,比如

  Object 和 String。ExpandoMetaClass

  对于其他一些用途也是有用的,比如将方法添加到对象实例,以及改善异常处理。

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

  从将行为附加到类的那一刻起,使用 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 属性是对 清单 4 的 randomize() 方法的改进:

  清单 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 的命名约定,将属性自动映射到 get 和

  set 方法,我让 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. 混合 ExpandoMetaClass 和

  methodMissing

  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,可以访问最细粒度的底层抽象,从而改变生成代码的方式。