1. 介绍
本篇内容为Groovy学习笔记第28篇,继续学习闭包相关知识。前面介绍了闭包的创建与使用,以及闭包的委托等和授权策略。
今天继续深入学习闭包相关知识。
2. GStrings中的闭包
我们知道在Groovy中有两种字符串对象,一个是java.lang.String
, 一个是groovy.lang.GString
。关于这两个字符串的对象。我在3. Groovy 语法-字符串学习 (zinyan.com)中有详细介绍。这里就不多讲了。
这里主要讲在闭包中的GStrings对象。我们如果要创建一个GString对象,示例代码如下:
代码语言:javascript复制def x = 1
def gs = "x = ${x}"
println gs // 输出:x = 1
而我们如果修改x的值,这个时候gs 并不会实时发生变化的。示例如下:
代码语言:javascript复制x = 2
println gs //输出:x = 1
我们会发现gs的值并没变成x=2,因为GString只懒惰地计算值的toString表示,在GString创建时GString中的语法{x}并不表示闭包,而是x的表达式。
所以,并不是所有花括号表达的就是闭包对象,要注意区分。
在上面的例子中,GString
是用一个引用x
的表达式创建的。当GString
被创建时,x
的值是1,因此GString
的值是1。当执行println
时,GString
被求值,并且使用toString
将1转换为String
。当我们将x
更改为2时,我们确实更改了x
的值,但它是一个不同的对象,GString
仍然引用旧的对象。这也就是为什么最后还是输出:x=1
的原因了。当GString
引用的值发生变化时,GString
才会改变它的toString
表示。如果引用改变,什么也不会发生。
如果你需要一个在闭包中可以发生变化的GString
,例如强制延迟计算变量,你需要使用替换语法${→x}
。示例如下:
def x = 1
def gs = "x = ${-> x}"
println gs //输出:x = 1
x = 2
println gs //输出:x = 2
让我们用下面的代码来说明它说明一下,普通的GString的变化:
代码语言:javascript复制//创建一个类
class Person {
//定义了一个String类型的name属性
String name
String toString() { name }
}
//创建对象
def zinyan = new Person(name:'zinyan.com')
def zStuden = new Person(name:'Z同学')
def p = zinyan
def gs = "Name: ${p}"
println gs //输出:Name: zinyan.com
p = zStuden //将对象p 的引用指向zStuden
println gs //输出:Name: zinyan.com
zinyan.name = 'zinyan'
println gs; //输出:Name: zinyan
因此,如果不想依赖于突变对象或包装对象,你必须在GString
中使用闭包,显式声明一个空参数列表:
//创建一个类
class Person {
//定义了一个String类型的name属性
String name
String toString() { name }
}
//创建对象
def zinyan = new Person(name:'zinyan.com')
def zStuden = new Person(name:'Z同学')
def p = zinyan
def gs = "Name: ${ -> p}" //这个时候就变成了一个带闭包的GString对象了
println gs //输出:Name: zinyan.com
p = zStuden //将对象p 的引用指向zStuden
println gs //输出:Name: Z同学
zinyan.name = 'zinyan'
println gs; //输出:Name: Z同学
总结:我们如果在
GString
中通过${X}
来引用变量。当GString
生成完毕后,后面再变化X
的数据,并不会影响GString
的结果。 我们如果想在GString
中添加可以动态变换的,就使用闭包引用的方式${→x}
3. 强制转换
闭包可以转换为接口或单一抽象方法类。前面学习过as
关键字。我们可以将闭包对象通过as
关键字转换为接口对象或单一抽象方法的类对象。
示例如下:
代码语言:javascript复制//创建一个接口对象
interface Predicate<T> {
boolean accept(T obj)
}
//创建一个单一抽象方法类
abstract class Greeter {
abstract String getName()
void greet() {
println "Hello, $name"
}
}
Predicate filter = { it.contains 'G' } as Predicate
assert filter.accept('Groovy') == true
Greeter greeter = { 'Groovy' } as Greeter
greeter.greet()
而从Groovy 2.2.0开始,as Type
表达式是可选的。你可以省略它,简单地写:
Predicate filter = { it.contains 'G' }
assert filter.accept('Groovy') == true
Greeter greeter = { 'Groovy' }
greeter.greet()
3.1 任意类型的强制闭包
除了SAM类型之外,闭包还可以被强制为任何类型和特定接口。示例如下:
代码语言:javascript复制//定义一个接口对象
interface FooBar {
int foo()
void bar()
}
我们可以使用as
关键字强制一个闭包进入接口:
def impl = { println 'ok'; 123 } as FooBar
assert impl.foo() == 123
impl.bar()
将生成一个类,其中所有方法都使用闭包实现。
4. 函数式编程
闭包,就像Java 8中的lambda
表达式一样,是Groovy
中函数式编程范式的核心。函数上的一些函数式编程操作可以直接在Closure
类上使用。
4.1 局部套用-curray
在Groovy
中,套用指的是部分应用的概念。由于Groovy
对闭包应用了不同的作用域规则,所以它并不符合函数式编程中curry
的真正概念。在Groovy中curry
将允许您设置闭包的一个参数的值,它将返回一个接受一个少参数的新闭包。
左侧套用:就是设置闭包最左边的参数,就像下面的例子:
代码语言:javascript复制//创建一个闭包对象,传入int 和String 参数,并计算来两者相乘的结果
def nCopies = { int n, String str -> str*n }
def twice = nCopies.curry(2)
println twice('zinyan') // 输出:zinyanzinyan
println twice('zinyan')==nCopies(2,'zinyan') //输出:true
例如上面的示例中,我们本来需要往闭包中传入两个参数,但是我使用curry
只传入最左边的参数,而右边的参数可以在之后进行传入。
右侧套用:类似于左curry
,可以设置闭包最右边的参数,使用关键方法为rcurry
:
def nCopies = { int n, String str -> str*n }
def blah = nCopies.rcurry('zinyan.com')
println blah(2) //输出:zinyan.comzinyan.com
那么,我们如果有多个参数,例如三个入参该怎么用?那就需要使用基于索引的套用:
代码语言:javascript复制//创建一个三个入参的闭包对象,计算三者相乘的结果
def volume = { double l, double w, double h ->
println "参数结果:l=$l,w=$w,h=$h" //输出:参数结果:l=3.0,w=2.0,h=4.0
l*w*h }
//创建一个基于索引的curry,例如下面,在索引1的地方插入参数
def fixedWidthVolume = volume.ncurry(1, 2d)
//补充剩下的参数,并输出
println fixedWidthVolume(3d,4d) //输出结果:24.0
//在索引1的地方开始,插入多个参数。
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d)
println fixedWidthAndHeight(3d) //输出:24.0
4.2 记忆化-memoize
记忆化允许对闭包调用的结果进行缓存。如果一个函数(闭包)的计算很慢,但这个函数将经常使用相同的参数被调用。
一个典型的例子是斐波那契集合。一个简单的实现可能是这样的:
代码语言:javascript复制def fib
//创建一个闭包对象,进行斐波那契数列, 方法中使用了递归,可以看到fib对象被递归调用
fib = { long n -> n<2?n:fib(n-1) fib(n-2) }
println fib(10) // 输出55
这是一个简单的实现,因为'fib
'经常用相同的参数递归调用,导致一个指数算法:
- 计算
fib(15)
需要fib(14)
和fib(13)
的结果 - 计算
fib(14)
需要fib(13)
和fib(12)
的结果
等等,需要前面的计算结果,一步步计算才能达到最终的结果。
由于调用是递归的,可以看到我们在上面将一次又一次地计算相同的值,这个简单的实现可以通过使用memoize
缓存调用的结果来提高效率:
def fib
//创建一个闭包对象,进行斐波那契数列,使用了memoize进行记忆缓存
fib = { long n -> n<2?n:fib(n-1) fib(n-2) }.memoize()
println fib(25) // 输出:75025
PS:缓存使用实参的实际值工作。这意味着,如果对除原语类型或方框原语类型以外的其他类型使用记忆,则应该非常小心。
缓存的行为可以使用其他方法来调整:
memoizeAtMost
:将生成一个新的闭包,它最多缓存n个值memoizeAtLeast
:将生成一个新的闭包,它至少缓存n个值memoizeBetween
:将生成一个新的闭包,它至少缓存n个值,最多缓存n个值
所有memoize
变体中使用的缓存都是LRU
缓存。
4.3 组合
闭包组合对应于函数组合的概念,也就是说,通过组合两个或多个函数(链接调用)来创建一个新函数,如下例所示:
代码语言:javascript复制def plus2 = { it 2 } // 创建一个闭包对象, it是默认传参的替代词
def times3 = { it * 3 }
// 两个闭包对象的结果进行 移位运算(https://zinyan.com/?p=395)
def times3plus2 = plus2 << times3
println times3plus2(4) //输出:14
println times3plus2(4) == plus2(times3(4)) //输出true
// 两个闭包对象的结果进行 移位运算(https://zinyan.com/?p=395)
// 移位运算符 左右两边的顺序是会影响结果的
def plus2times3 = times3 << plus2
println plus2times3(5) //输出:21
println plus2times3(5) == times3(plus2(5)) //输出:true
// 反向合成
println times3plus2(3) == (times3 >> plus2)(3) //输出:true
有关组合的内容,通过示例进行理解了。
4.4 弹性变换-trampoline
递归算法通常受到一个物理限制:最大堆栈高度。例如,如果我们调用了一个递归调用自身过深的方法,您最终将收到一个StackOverflowException
。
例如我们上面介绍的斐波拉契数计算。我们填写1024,就会由于超过堆栈触发
StackOverflowException
在这种情况下,一种有用的方法是使用Closure
及其弹性变换功能。
Closure
被包裹在TrampolineClosure
中。在调用时,TrampolineClosure
将调用等待其结果的原始闭包。如果调用的结果是TrampolineClosure
的另一个实例(可能是调用trampoline()
方法的结果),则闭包将再次被调用。这种对返回的TrampolineClosure
实例的重复调用将持续下去,直到返回一个非TrampolineClosure
的值。这个值将成为最终结果。这样,调用是串行进行的,而不是填充堆栈。
下面是一个使用trampoline()
实现阶乘函数的例子:
def factorial
factorial = { int n, def accu = 1G ->
if (n < 2) return accu
//使用trampoline 进行弹性控制
factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline()
println factorial(1) //输出:1
println factorial(3) // 输出:6
println factorial(1000) //输出:402387260077093773543702433923003985719374864210714632543799910429938512....
4.5 方法指针
能够使用常规方法作为闭包通常是实用的。例如,可能希望使用闭包的curry
功能,但普通方法不具备这些功能。在Groovy中,可以使用方法指针操作符从任何方法获得闭包。
方法指针关键字为:.&
操作符,它可以用于在变量中存储对方法的引用。可以通过我的这篇文章了解方法指针运算符8. Groovy 运算符-条件运算符,对象运算符学习 (zinyan.com)
5. 小结
到这里,Groovy中关于闭包的相关基础知识就介绍完毕了。下一篇将开始学习和分享Groovy语义相关内容。
以上内容可以通过Groovy官方文档:Groovy Language Documentation (groovy-lang.org) 进行学习了解。
如果觉得总结的还比较清晰,希望能够给我点个赞鼓励一下。谢谢。