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),它包括 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 中最后三行所示。
|
|