介绍
Groovy 中的闭包是一个匿名的代码块,可以接受参数,并返回一个返回值,也可以引用和使用在它周围的,可见域中定义的变量。
在许多方面,它看起来像 java 中的匿名内部类,并且闭包的用法也确实像大多数 java 开发者使用匿名内部类的方式。但事实上,Groovy 的闭包要比 java 的匿名内部类强大,并且更加便于使用。
用函数式语言的说法,这样的匿名代码块,可以被引用为通常的匿名 lambda 表达式,或者是一个未绑定任何变量的 lambda 表达式,或者是封闭的 lambda 表达式,如果它没有包含对未绑定变量的引用的话。Groovy 并不作这些区分。
更严格的说,一个闭包是不可以被定义的。你可以定义一个代码块,使它引用本地变量或者成员属性,但是只有当它被绑定(赋予它一个含义)到一个变量时,它才成其为一个闭包。闭包是一个语义概念,就像实例一样,你不可以定义,但可以创建。更严格意义上的闭包,其所有的自由变量都将被绑定,否则只是部分闭合,也就不是一个真正的闭包。虽然 Groovy 并没有提供一个途径来定义闭合的 Lambda 函数,并且一个代码块也可能根本就不是一个闭合的 Lambda 函数(因为它可能有自由变量),但我们还是认为它们是一个闭包--就像一个语法概念一样。我们之所以称它为语法概念,这是因为代码的定义和实例的创建是一体的,这里没有什么不同。我们非常清楚的知道,这个术语的使用或多或少是错误的,但是在讨论某种语言的代码的时候,却可以简化很多事情,并且不需要深究这些差异。
闭包的正式定义语法
闭包的定义,可采用下面的方式:
{ [closureArguments->] statements }
这里的 [closureArguments->] 是一个可选的,逗号分隔的参数列表。statements 由 0个或多个 Groovy 语句构成。参数列表看起来有点像方法的参数列表,它们可能带类型声明,可能不带。如果指定了一个参数列表,则 符号 -> 必须出现,以分离参数列表和闭包体。statements 部分可以由 0 个,1个,或很多的 Groovy语句构成。
下面是一些有效的闭包定义方式:
{ item++ }
{ println it }
{ ++it }
{ name -> println name }
{ String x, int y -> println "hey ${x} the value is ${y}" }
{ reader ->
while (true) {
def line = reader.readLine()
}
}
闭包语义
闭包看起来像是一种方便的机制,用来定义一些东西,如内部类,但它的语义其实要比内部类提供的更强大和微妙。特别的,闭包的特性可以简要介绍如下:
1,它们拥有一个隐含的方法(在闭包的定义中从来不指定)叫 doCall()。
2,一个闭包可以被 call() 方法调用,或者通过一种特殊的无名函数 () 的语法形式来调用。这两种调用方式都会被 Groovy 转换成对闭包的
doCall() 方法的调用。
3,闭包可以接收 1....N 个参数,这些参数可以静态的声明其类型,也可以不声明类型。第一个参数是一个未声明类型的隐含变量叫 “it” ,
如果没有其他显式的参数声明的话。如果调用者没有声明任何参数,则第一个参数(并且扩展为 it)将为 null。
4,开发者不需要一定用 it 作为第一个参数,如果你想用一个不同的名字的话,可以在参数列表中声明。
5,闭包总是返回一个值,要么显式地使用 return 语句,要么隐含地返回闭包体中最后一个语句的值。(也就是说,显式地 return 语句是可选
地)
6,闭包可以引用任何定义在它的封闭词法域中的变量,我们称这样的变量被绑定在闭包上。
7,即使一个闭包在它的封闭词法域外返回,那些绑定在这个闭包上的变量仍旧可以被这个闭包使用。
8,闭包在 Groovy 中是第一等公民,并且总是从类 Clousre 继承。代码可以通过一个无类型变量,或者一个类型为 Closure 的变量来
引用闭包。
9,一个闭包体只有在它被显式地调用时,才被执行,即闭包在它的定义处是不被执行的。
10,一个闭包可能被调味,因此一旦发生实例拷贝,则它的一个或多个参数会被固定为某个值。
这些特性将在下面的章节中进一步解释。
闭包是匿名的
闭包在 Groovy 中总是匿名的,不像 java 或 Groovy 的类,你不可能得到一个命名的闭包。你只能通过一个无类型变量,或类型为 Closure 的变量来引用闭包,并且把这个引用当参数传递给方法,或传递给其他闭包。
隐含方法
闭包可以被认为拥有一个隐含定义的方法,它对应于这个闭包的参数和闭包体。你不可以重载或者重定义这个方法。这个方法总是通过闭包的 call() 方法来调用,或者通过一个特殊的无名函数 () 的语法来调用。这个隐含方法的名字是 doCall() 。
闭包的参数
闭包总是含有至少一个输入参数,名字叫 it ,可以在闭包体内使用,除非定义了其他显性的参数。开发者不需要显式的声明 it 变量,就像对象中的 this 引用一样,它是隐含可用的。
如果一个闭包以 0个参数的方式被调用,则 it 的值将为 null。
开发者可以给闭包传递显式声明的参数列表,这个参数列表包含一个或多个参数名称,其间用逗号隔开。参数列表的结束由符号 “->”标识。每个参数即可以不带任何类型声明,也可以静态的指定一个类型。如果一个显式的参数表被声明,则 it 变量将不可用。
如果参数声明了类型,则这个类型将在运行期被检查。如果在闭包的调用中,有一个或多个参数的类型不匹配,则将抛出一个运行期异常。注意,这个类型检查总是发生在运行期,这里没有静态的类型检查,编译器是不会报告任何类型不匹配错误的。
Groovy 特别支持溢出参数。一个闭包可以把它的最后一个输入参数声明为 Object[] 类型,在调用时,任何多余的溢出参数将被放置在这个对象数组中。这可以被认为是对变长参数的一种支持,如:
def c = {
format, Object[] args ->
aPrintfLikeMethod (format, args)}
c ("one", "two", "three");
c ("1");
上面例子中,两种 c 的调用都是可用的。由于上面的闭包定义了两个输入参数: fomat 和 args,并且 args 是一个Object[] 类型对象,因此对这个闭包的任何调用中,第一个参数总是绑定在 format 上,其余的参数则绑定在 args 上。在上面的第一种场景中,参数 args 将接收两个值
"two"和 "three",而参数 format 将接收 “one”;而在第二个调用情形下,参数 args 将接收 0 个元素,而 format 将接收值“1”。
闭包的返回值
闭包总是拥有一个返回值,要么是闭包体中显式的用一个或多个 return 语句来实现,要么将最后一个执行的语句的值作为返回值,前提是没有显式的指定任何 return 语句。如果最后一个执行的语句没有返回值(如调用了一个 void 类型的方法),则返回 null。
目前还没有机制,可以静态的指定一个闭包的返回值类型。
引用外部变量
闭包可以引用外部的变量,包括局部变量,方法参数和成员对象。闭包只能引用那些和闭包定义在同一个源文件中的,符合编译器词法演绎规则的变量。
一些例子有助于说得更清楚。下面的例子是有效的,并且展示了一个闭包对方法本地变量、方法参数的使用情况:
public class A {
private int member = 20;
private String method()
{
return "hello";
}
def publicMethod (String name_)
{
def localVar = member + 5;
def localVar2 = "Parameter: ${name_}";
return {
println "${member} ${name_} ${localVar} ${localVar2} ${method()}"
}
}
}
A sample = new A();
def closureVar = sample.publicMethod("Xavier");
closureVar();
结果是:
20 Xavier 25 Parameter: Xavier hello
我们来看一下类A的定义,方法 publicMethod 中的闭包,访问了该方法所有可以合法访问的变量。不管是访问本地变量,方法参数,成员实例,还是函数调用,都可以。
当闭包以这种方式引用变量时,这些变量就被绑定在这个闭包上。此时,这些变量在它们定义的词法域范围内,和通常情况一样,仍旧是可用的,不仅闭包可以读取修改这些变量,其他地方的合法代码也可以读取修改这些变量。
当闭包从它的封闭域返回时,与它绑定的那些变量仍旧存活着。绑定只发生在闭包被实例化时。如果对象方法或者成员实例,在一个闭包内被使用,则指向这些对象的引用将被保存在闭包内。如果一个本地变量或一个方法参数被引用了,则编译器将会覆盖这个本地变量或方法参数的引用,使其脱离堆栈区,而存储在堆区。
保持这样的认识很重要:这样的引用方式,必须符合编译器的词法结构规定(这个例子里是类 A)。这个过程并不会通过检索调用栈而动态发生。因此下面的用法是非法的:
class A {
private int member = 20;
private String method()
{
return "hello";
}
def publicMethod (String name_)
{
def localVar = member + 5;
def localVar2 = "Parameter: name_";
return {
// Fails!
println "${member} ${name_} ${localVar} ${localVar2} ${method()} ${bMember}"
}
}
}
class B {
private int bMember = 12;
def bMethod (String name_)
{
A aInsideB = new A();
return (aInsideB.publicMethod (name_));
}
}
B aB = new B();
closureVar = aB.bMethod("Xavier");
closureVar();
这个例子和第一个例子有些相像,但我们新定义了一个类 B,在这个类中动态地创建了一个类 A 的实例,然后调用了类 A 的 publicMethod 方法。紧接着,在 publicMethod 方法中的闭包,试图引用类 B 中的一个成员,但这是不允许的,因为编译器无法静态的判定访问可见性。一些老的语言通过在运行期动态检索调用栈,而允许这种引用方式,但在 Groovy 中是不被允许的。
Groovy 支持一个特殊的 owner 变量,当一个闭包的参数隐藏(挡住)了一个同名成员时,就会有用。如:
class HiddenMember {
private String name;
def getClosure (String name)
{
return { name -> println (name)}
}
}
在上面的代码中, println(name) 引用了方法参数 name。如果闭包想访问外围类的同名成员对象时,它可以通过 owner 变量来实现这个目的:
class HiddenMember {
private String name;
def getClosure (String name)
{
return { name -> println ("Argument: ${name}, Object: ${owner.name}")}
}
}
闭包类型
Groovy 中的所有闭包都继承自类型 Closure。在 Groovy 编程中,每个唯一的闭包定义,都会导致创建一个唯一的类,该类继承自类型 Closure。 如果你想显式的指定参数、本地变量、成员变量中的闭包类型,则必须使用 Cloure 类型。
一个闭包的确切类型通常不会被先定义,除非你显式的指明继承自类型 Cloure 的子类。看下面的例子:
def c = { println it}
上面的例子中,对闭包进行引用的变量 c 的具体类型并没有被定义,我们仅仅知道它是类型 Cloure 的某个子类。
闭包的创建和调用
当闭包的周围代码触及到它们时,闭包才被隐性的创建。例如在下面的例子中,有两个闭包被创建:
class A {
private int member = 20;
private method()
{
println ("hello");
}
def publicMethod (String name_)
{
def localVar = member + 5
def localVar2 = "Parameter: name_";
return {
println "${member} ${name_} ${localVar} ${localVar2} ${method()}"
}
}
}
A anA = new A();
closureVar = anA.publicMethod("Xavier");
closureVar();
closureVar2 = anA.publicMethod("Xavier");
closureVar2();
在上面的例子中,cloureVar 所持有的闭包对象引用,与 cloureVar2 的是不一样的。闭包通常就是以这样的方式隐性的创建--你不能通过
编程的方式来创建,如使用 new 操作符。
闭包的调用有两种方式,显性的调用方式是通过 call() 方法:
closureVar.call();
你也可以使用隐式的匿名调用:
closureVar();
如果你查看闭包的 javadoc 时,你会发现闭包的 call() 方法,在类型 Cloure 中的定义形式如下:
public Object call (Object[] args);
按照这个方法声明的样子,你无需手工的将参数转换成对象数组,调用还是按照通常的方式,Groovy 会自动做这个对象数组的转换:
closure ("one", "two", "three")
closure.call ("one", "two", "three")
上面的两种调用方式都是合法的。但是,如果你的闭包要和 java 代码交互,那么这个 Object[] 对象就不得不手工创建了。
通过调味(curry),将闭包参数预固定为某个数值
你可以通过使用 Cloure 对象的 curry() 方法,来把一个闭包实例的一个或多个参数固定为常量【译者注:就像食物/参数入口前,先进行一下调味,然后再送入胃部处理/运算。这个技巧结合数学中的复合函数的概念,可以实现很多巧妙的用法】。这样的行为在函数编程范式中通常称为调味(curring),调味所得到的结果(译注:一个新的闭包)通常称为调味闭包(Curried Cloure)。调味闭包可以用来创建泛型闭包,即在原始定义的基础上,通过将闭包参数进行不同的绑定,从而可实现几个不同的闭包版本。
当给一个闭包实例的curry() 方法传递一个或多个参数,并调用它时,一个闭包的拷贝将被首先建立。所有的输入参数将永久性的被绑定在这个新的闭包拷贝上,传递给 curry() 的参数 1….N 将被绑定到新闭包的 1….N 参数上。然后这个新的调味闭包将被返回给调用者。
调用者对新实例(调味闭包)进行调用时,所有的传入参数将从原始闭包的第(N+1)个参数开始匹配绑定。
def c = { arg1, arg2-> println "${arg1} ${arg2}" }
def d = c.curry("foo")
d("bar")
在上面的例子中定义了一个原始闭包 c ,然后调用了 c.curry(“foo”),它将返回一个调味闭包,调味闭包的第一个参数arg1被永久绑定为值“foo” 。当调用这个调味闭包 d(“bar”)时,参数 “bar”被传递给原始闭包的第二个参数 arg2,最后的输出结果是 “foo bar”
请参考:用Groovy 进行函数编程
特殊案例:把闭包作为参数传递给方法
Groovy 专门提供了一个语法便利,来方便把一个闭包定义为方法的参数,以增加可读性。特别的,如果一个方法的最后一个参数,是闭包类型的话,则可以在调用这个方法时,将闭包对象放在参数括号外。如下面的例子所示:
class SomeCollection {
public void each (Closure c)
}
然后我们可以在调用 each() 方法时,把闭包对象放在圆括号外面。
SomeCollection stuff = new SomeCollection();
stuff.each() { println it }
更传统的调用语法也是可用的。另外,由于在很多场合下,Groovy 允许省略圆括号,因此下面的两种变形语法也是合法的:
SomeCollection stuff = new SomeCollection();
stuff.each { println it } // Look ma, no parens
stuff.each ({ println it }) // Strictly traditional
这个规则甚至可用于方法的参数多余1个的情形,唯一的要求是闭包参数必须是最后一个参数:
class SomeCollection {
public void inject (x, Closure c)
}
stuff.inject(0) {count, item -> count + item } // Groovy
stuff.inject(0, {count, item -> count + item }) // Traditional
这个规则仅适用于闭包对象在方法调用中被显式的定义(为参数)的场景,而不能通过一个闭包类型的变量来作为参数传递:
class SomeCollection {
public void inject (x, Closure c)
}
counter = {count, item -> count + item }
stuff.inject(0) counter // Illegal! No Groovy for you!
如果你没有在函数调用参数中直接定义内联闭包对象,则不能使用上述语法,而必须使用更明晰的写法:
class SomeCollection {
public void inject (x, Closure c)
}
def counter = {count, item -> count + item }
stuff.inject(0,counter)
闭包和匿名内部类的对比
Groovy 之所以包含闭包,是因为它可以让开发者写出更简约、更易懂的代码。Java 开发者通常用一个单方法的接口(Runable,采用Command设计模式),并结合匿名内部类来实现;而Groovy 则允许以一种更简易、直白的方式来实现。额外的,相较匿名内部类,闭包有很少量的约束,包括一些额外的功能。
绝大部分的闭包都是简短的、孤立的、碎片状的代码,并执行特定的功能任务。语法书写的流畅性要求闭包的定义简短、易读,不凌乱。例如在 java 代码中经常看到下面的类 GUI 代码:
Button b = new Button ("Push Me");
b.onClick (new Action() {
public void execute (Object target)
{
buttonClicked();
}
});
同样的代码在 Groovy 看起来是这样:
Button b = new Button ("Push Me");
b.onClick { buttonClicked() }
完成同样的任务,Groovy 代码看起来更清晰,更整洁。这是 groovy 闭包的第一个准则--闭包要简练、易于书写。另外,闭包可以引用它的定义域外围的变量,而匿名内部类则有限制,更进一步,这样的变量不需要是 final 限定的。
闭包会携带和它相关的所有状态,甚至在它们引用本地变量或入口参数时,也是这样。闭包同样也符合 Groovy 的动态语言类型的优点,因此你无需声明作为参数,或作为返回值的闭包的类型(实际上,闭包可以在多层调用中携带大量的参数)。
Groovy 闭包在和采用Command 设计模式的接口的比较中,比较薄弱的就是静态类型的调用。在 java 接口中会强制指定对象的类型,及可以调用的方法集合。而在Groovy 中,所有的闭包都是 Cloure 类型,并且参数类型的检查是在运行期进行的。
闭包作为Map 的键和值
闭包作为键
你可以把闭包作为Map 的键,但必须用括号转义处理(否则可能会被看作字符串)。像下面所示:
f = { println "f called" }
m = [ (f): 123 ]
当以闭包作为键访问 Map 中的值时,必须使用方法 get(f),或者 m[f] 方式。“m.f”方式将被认作字符串。
println m.get(f) // 123
println m[f] // 123
println m.f // null
闭包作为值
闭包可以作为 Map 中的值存在,并且可以调用执行它,就像是调用 Map 对象的一个扩展方法一样:
m = [ f: { println 'f called' } ]
m.f() // f called
m = new Expando( f: { println 'f called' } )
m.f() // f called
通过 use 指令来扩展groovy
你可以提供自定义的、特殊的方法来支持闭包,只要在一个 java 类中包含并实现这个方法。这些方法必须是静态的,并且至少有两个参数。第一个参数的类型必须是这个方法要操作的类型,最后一个参数必须是一个 Cloure 类型。
看下面的例子,这是一个 eachFile() 方法的变种,它忽略文件,而仅仅打印目录对象中的目录:
dir = new File("/tmp")
use(ClassWithEachDirMethod.class) {
dir.eachDir {
println it
}
}
注意 use() 指令,它告诉 Groovy 方法 eachFile() 是在哪个类中定义并实现的。下面就是这个方法的 java 实现以支持新的 eachFile () 功能:
public class ClassWithEachDirMethod {
public static void eachDir(File self, Closure closure) {
File[] files = self.listFiles();
for (int i = 0; i
分享到:
相关推荐
### Groovy Script 入门知识点详解 #### 一、Groovy脚本简介 Groovy是一种灵活的面向对象的编程语言,它运行在Java平台上。由于其语法简洁且与Java高度兼容,因此对于Java开发者来说非常容易上手。Groovy不仅支持...
### Groovy介绍与官网资源详解 #### 一、Groovy简介 Groovy是一种灵活的、面向对象的编程语言,主要用于Java虚拟机(JVM)。它兼容现有的Java代码,并且能够直接调用Java类库,这使得Groovy成为扩展Java应用程序的...
### Groovy速查手册知识点详解 #### 一、Groovy简介与特性 Groovy是一种为Java虚拟机(JVM)设计的动态语言。它具备完全的对象导向性、可选的类型系统、操作符定制能力、简洁的数据类型声明、闭包(Closures)、...
### Groovy之旅知识点详解 #### 一、Groovy背景 **1.1 Groovy是什么?** Groovy是一种灵活且强大的编程语言,它被设计用于Java平台,并与Java完全兼容。这意味着开发者可以在现有的Java项目中轻松引入Groovy代码...
#### 三、Groovy主要特性详解 1. **Closure(闭包)支持**:闭包是Groovy的一个核心特性,允许定义无名函数,通常作为方法参数传递。 - **定义与使用**:闭包使用大括号`{}`定义,参数列表放在闭包体前,使用竖线`...
### Groovy 快速入门指南知识点详解 #### 一、集合操作 Groovy 提供了对集合的强大支持,包括 `List` 和 `Map` 的多种操作方式。 **1. List** - **定义与访问** - Groovy 中的 `List` 可以包含不同类型的元素。...
"Android Studio 中运行 Groovy 程序的方法图文详解" Android Studio 中运行 Groovy 程序的方法可以分为以下几个步骤: 1. 新建一个 Java Library 模块 2. 修改该模块下的 build.gradle 文件,添加 Groovy 插件和...
- Groovy 中的控制流语句详解,例如条件判断、循环等。 - 如何使用 Groovy 进行简单的文本处理和文件操作。 2. **进阶篇**: - 面向对象编程特性,包括类、接口、继承、多态等。 - 闭包和元编程的深入探讨,...
### 关于《Programming Groovy 2》的知识点详解 #### 一、Groovy语言概述 Groovy是一种基于Java平台的动态编程语言,它为开发者提供了一种简洁且强大的语法结构来构建应用程序。Groovy的设计旨在与Java代码无缝...
**Grails 框架与 Groovy 语言详解** **一、Grails 框架** Grails 是一个基于 JVM 的开源全栈式Web应用框架,它构建在Groovy语言之上,提供了一种高效且富有生产力的开发环境。Grails 的设计理念是"Convention over...
3. **闭包**:Groovy 引入了闭包的概念,这是一种强大的功能,常用于函数式编程。 4. **集成Java**:Groovy 可以无缝地与Java代码交互,可以直接调用Java库和API。 **Grails 架构和核心组件** 1. **GORM (Grails ...
### Groovy 快速入门知识点详解 #### 一、Groovy基础语法介绍 Groovy是一种灵活的编程语言,运行在Java平台上,具有简洁且强大的特性。Groovy支持面向对象编程和函数式编程,并且能够与Java无缝集成。下面将详细...
### 简易Groovy教程知识点详解 #### 一、Groovy概述 - **定义**:Groovy是一种灵活且强大的编程语言,它结合了Python、Ruby等动态语言的特点,同时保持了与Java平台的高度兼容性。这意味着对于Java开发者来说,学习...
### Groovy实例代码详解 #### 一、字符串处理 Groovy是一种强大的脚本语言,尤其在处理字符串方面提供了丰富的功能。下面将详细解析Groovy在字符串处理中的几个关键知识点: 1. **访问子字符串** - Groovy允许...
### Groovy 入门知识点详解 #### 一、Groovy 概述 - **定义**:Groovy 是一种运行在 Java 虚拟机 (JVM) 上的动态编程语言,它结合了 Java 和 Ruby 的优点,使得开发更加高效、灵活。 - **特点**: - 与 Java 无缝...
3. **GROOVY入门经典.pdf**:这本书可能会介绍Groovy的基本语法、面向对象特性、闭包以及如何利用Groovy增强Java应用。Groovy与Java的互操作性使得它在构建脚本和自动化任务时非常有用。 4. **AXIOM 数据模拟器 V...
闭包在Groovy中广泛用于简化迭代和其他复杂的编程模式。 #### 与Java的交互 - **无缝集成**:Groovy编写的代码可以直接编译成Java字节码,从而可以在Java环境中运行。 - **双向重用**:用Groovy编写的代码可以在...