Groovy 的閉包

如下內容大部分來自 Groovy 官方文檔,英文能力 OK 的同窗建議直接查看官方文檔。 相關 Groovy 示例已經上傳到 zjxstar 的 GitHubhtml

前言

在 Java 中,一般以建立匿名內部類的方式來定義用於註冊事件處理器的方法參數,但這會使得代碼變得很是冗長。而 Groovy 中的閉包能夠去掉這種冗長感。閉包是輕量級的、短小的、簡潔的,是 Groovy 中最重要、最強大的特性之一。java

Groovy 的閉包 ( Closure ) 是一個開放的匿名代碼塊。閉包也是對象,它能夠傳遞參數,能夠返回值並分配給變量。Groovy 中的閉包甚至能夠打破閉包的正式概念,即:能夠包含其做用域外的自由變量。git

閉包帶來的好處

Groovy 中的閉包避免了代碼的冗長,能夠輔助建立輕量級、可複用的代碼片斷。下面舉個簡單的例子帶你們體驗一下閉包的便捷性。github

例子:求 1 到 n 之間的全部奇數的和。算法

傳統實現中,咱們定義一個 sum 方法,其中使用 for 循環實現奇數的累加,代碼以下:編程

def sum(n) {
    total = 0
    for (int i = 1; i <= n; i += 2) {
        // 計算1到n的奇數和
        total += i
    }
    total
}

println(sum(9)) // n等於9
複製代碼

若是改爲計算全部奇數的乘積呢?那又得定義一個 multiply 方法,仍是使用 for 循環實現,代碼以下:數組

def multiply(n) {
    total = 1
    for (int i = 1; i <= n; i += 2) {
        // 計算1到n的奇數乘積
        total *= i
    }
    total
}
複製代碼

若是再換成求奇數的平方和呢?實現代碼基本與 sum 和 multiply 一致,重複的 for 循環,不一樣的計算方式而已。緩存

僅僅是三種狀況就定義了三個方法,並且存在大量重複代碼,這種實現太不優雅了。那使用閉包實現呢?咱們只須要定義一個高階函數,傳入一個 Closure 參數便可。閉包

def pickOdd(n, closure) { // 此處的closure能夠換成別的參數名
    for (int i = 1; i <= n; i += 2) {
        // 執行邏輯都由傳入的閉包決定
        closure(i)
    }
}
複製代碼

Groovy 的閉包能夠附在一個方法上或者賦值給一個變量。示例中變量 closure 就保持了一個指向閉包的引用。使用 pickOdd 方法的時候傳入包含不一樣邏輯的閉包方法塊便可。函數式編程

// 打印奇數
pickOdd(9) {
    println it // 下文會詳細介紹it
}

// 求和
total = 0
pickOdd(9, {
    num -> total += num // 能夠訪問total變量
})
println("Sum of odd is: ${total}")
複製代碼

顯然,經過閉包實現不只在語法上比傳統方式更優雅,還爲函數將部分實現邏輯委託出去提供了一種簡單、方便的方式。

定義和使用閉包

前一節中簡單實現了兩個閉包。直觀上,所謂的閉包就是使用大括號包裹的代碼塊。本節會詳細介紹 Groovy 中閉包的定義語法以及使用方式。

定義閉包

閉包的定義語法很簡單(有點相似 Java 8 中的 Lambda 表達式):

{ [closureParameters -> ] statements }
複製代碼

語法中,[ closureParameters -> ] 表明參數列表,參數能夠是 0 個或多個,且是否聲明參數類型是可選的。若是聲明瞭參數列表,則 -> 是必須存在的,用以區分參數和執行語句。statements 表明執行語句,通常狀況下都有至少一條執行邏輯(能夠沒有)。固然,你也能夠寫一個空的閉包 { } ,但這沒什麼意義。

如下是一些合法的閉包示例:

{} // 接收一個參數,空執行語句

{ ++it } // 接收一個參數,默認it

{ -> println('no param')} // 沒有參數,必須寫->

// 能夠聲明參數類型
{ String x, int y -> println("x is ${x}, y is ${y}") }

// 能夠省略參數類型
{ x, y -> println("x is ${x}, y is ${y}") }

// 一個參數,多行執行語句
{ x ->
    int y = -x
    println("-x is ${y}")
}
複製代碼

前文提到過閉包也是對象,沒錯,它是 groovy.lang.Closure 類的實例。因此咱們能夠將其賦值給變量。

def closure4 = { x, y -> println("x is ${x}, y is ${y}") } // 省略類型
Closure closure6 = { println('Done') } // 聲明類型Closure
Closure<Boolean> closure7 = { int a, int b -> a == b } // 能夠定義返回類型
複製代碼

使用閉包

閉包的使用很是簡單,咱們能夠像方法同樣調用一個閉包,也能夠顯式地經過 call 方法調用。

def isOdd = { int i -> i%2 != 0 }
assert isOdd(3) == true // 相似方法調用
assert isOdd.call(2) == false // 使用call方法
複製代碼

閉包在用法上與方法有些相似,但仍是有區別的。閉包被執行時老是有返回值的,默認狀況下會返回 null

Closure closure6 = { println('Done') }
assert closure6() == null // 返回null

def code = { 123 } // 顯式返回123
assert code() == 123
複製代碼

向閉包傳遞參數

定義閉包時能夠設置參數,調用時能夠和調用普通方法同樣傳遞參數。閉包中的參數分爲三個類型:通常參數、隱藏參數和變長參數,下面詳細介紹這三類參數。

通常參數

通常參數的設置原則和 Groovy 中方法設置參數的原則保持一致:

  • 參數類型是可選的;

  • 必須有參數名;

  • 參數能夠有默認值,即默認參數;

  • 多個參數之間以逗號分隔;

舉例說明:

// 一個參數,不聲明參數類型
def closureWithOneArg = { str -> str.toUpperCase() }
assert closureWithOneArg('groovy') == 'GROOVY'

// 一個參數,聲明參數類型
def closureWithOneArgAndExplicitType = { String str -> str.toUpperCase() }
assert closureWithOneArgAndExplicitType('groovy') == 'GROOVY'

// 多個參數,逗號分隔
def closureWithTwoArgs = { a,b -> a+b }
assert closureWithTwoArgs(1,2) == 3

// 多個參數,都聲明參數類型
def closureWithTwoArgsAndExplicitTypes = { int a, int b -> a+b }
assert closureWithTwoArgsAndExplicitTypes(1,2) == 3

// 多個參數,部分聲明參數類型
def closureWithTwoArgsAndOptionalTypes = { a, int b -> a+b }
assert closureWithTwoArgsAndOptionalTypes(1,2) == 3

// 多個參數,有默認參數
def closureWithTwoArgAndDefaultValue = { int a, int b=2 -> a+b }
assert closureWithTwoArgAndDefaultValue(1) == 3
複製代碼

隱藏參數

在前文的例子中常常看到 it 關鍵字,沒錯,它就是 Groovy 閉包提供的隱藏參數。當閉包沒有顯示定義參數列表時( 隱藏 -> 符號時 ),閉包會默認提供一個名爲 it 的參數,能夠在執行語句中使用該參數。此時,這個閉包接受一個參數的傳遞。

// greeting1 和 greeting2 是等價的
def greeting1 = { "Hello, $it!" }
assert greeting1('Groovy') == 'Hello, Groovy!'

def greeting2 = { it -> "Hello, $it!" }
assert greeting2('Groovy') == 'Hello, Groovy!'
複製代碼

若是但願閉包不接受任何參數,則必須顯式地聲明 -> 符號。

def magicNumber = { -> 42 }
println(magicNumber())
//magicNumber(11) // 錯誤,不接受參數
複製代碼

變長參數

咱們在 Java 和 Groovy 的方法中都使用過變長參數,只需在參數類型後面添加 ... 符號便可表示變長參數。Groovy 的閉包中也是一樣的用法,直接看例子。

def concat1 = { String... args -> args.join('') }
assert concat1('abc','def') == 'abcdef'
// 效果和數組參數同樣
def concat2 = { String[] args -> args.join('') }
assert concat2('abc', 'def') == 'abcdef'

// 變長參數必須放在參數列表的最後
def multiConcat = { int n, String... args ->
    args.join('')*n
}
assert multiConcat(2, 'abc','def') == 'abcdefabcdef'
複製代碼

閉包的委託策略

閉包雖然是從 Lambda 表達式演變而來,但與之最大的不一樣是閉包有委託的概念。閉包中修改委託對象和委託策略的能力,可以讓它在 DSLs ( Domain Specific Languages ) 領域大放異彩。

要充分理解閉包的委託功能,就不得不先學習它的三個屬性了:this、owner 和 delegate。

this

this 對應了定義閉包的所在封閉類。

在閉包中,能夠經過 getThisObject 方法獲取定義閉包的封閉類,這和顯式調用 this 是等價的。

// 普通類
class Enclosing {
    void run() {
        // getThisObject指向Enclosing
        def whatIsThisObject = { getThisObject() }
        assert whatIsThisObject() == this
        println('getThisObject(): ' + whatIsThisObject())
        
        // this指向Enclosing
        // this和getThisObject是等價的
        def whatIsThis = { this }
        assert whatIsThis() == this
        println('this in Closure: ' + whatIsThis())
        println('this: ' + this)
    }
}

// 內部類
class EnclosedInInnerClass {
    class Inner {
        // 此處的this指向Inner,而不是EnclosedInInnerClass
        Closure cl = { this }
    }
    void run() {
        def inner = new Inner()
        assert inner.cl() == inner
        println('Closure in Inner, this: ' + inner.cl())
    }
}

// 嵌套閉包
class NestedClosures {
    // this始終指向NestedClosures
    void run() {
        def nestedClosures = {
            def cl = { this }
            println("cl this: " + cl())
            cl()
        }
        assert nestedClosures() == this
        println("nestedClosures this: " + nestedClosures())
        println("this: " + nestedClosures())
    }
}
複製代碼

從示例中能夠看出,this 始終指向着定義閉包的封閉類,也就是這個封閉類的一個實例。

owner

owner 對應了定義閉包的封閉對象,能夠是類或者閉包。它與 this 有點相似,最大的不一樣是 owner 指向的是直接包裹它閉包的對象,這就比 this 多了一種可能——閉包對象。

// 普通類
class EnclosingOwner {
    void run() {
        // getOwner指向EnclosingOwner
        def whatIsOwnerMethod = { getOwner() }
        assert whatIsOwnerMethod() == this
        // owner指向EnclosingOwner
        def whatIsOwner = { owner }
        assert whatIsOwner() == this

        println('getOwner: ' + whatIsOwnerMethod())
        println('owner: ' + whatIsOwner())
        println('this: ' + this)
    }
}

// 內部類
class EnclosedInInnerClassOwner {
    class Inner {
        // 指向Inner
        Closure cl = { owner }
    }
    void run() {
        def inner = new Inner()
        assert inner.cl() == inner
        println('inner owner: ' + inner.cl())
    }
}

// 嵌套閉包
class NestedClosuresOwner {
    void run() {
        def nestedClosures = {
            // 指向的就是nestedClosures閉包對象了,而不是NestedClosuresOwner
            // 這裏就和this不同了
            def cl = { owner }
            println('Closure owner: ' + cl())
            println('Closure this: ' + this)
            cl()
        }
        assert nestedClosures() == nestedClosures
    }
}
複製代碼

delegate

delegate 能夠對應任何對象。this 和 owner 都屬於閉包的詞法範圍,而 delegate 指向的是閉包所使用的用戶自定義對象。默認狀況下,delegate 被設置成 owner 。咱們能夠經過 delegate 關鍵字和 getDelegate 方法來使用委託對象。

class EnclosingDelegate {
    void run() {
        // 指向的是EnclosingDelegate ,與this、owner表現一致
        def cl = { getDelegate() }
        def cl2 = { delegate }
        assert cl() == cl2()
        assert cl() == this
        println(cl())

        // 指向的是enclosed ,與owner表現一致
        def enclosed = {
            { -> delegate }.call()
        }
        assert enclosed() == enclosed
        println(enclosed())
    }
}
複製代碼

delegate 能夠被修改指向任意對象。

class Person {
    String name
}
class Thing {
    String name
}

def p = new Person(name: 'Tom')
def t = new Thing(name: 'Teapot')

def upperCasedName = { delegate.name.toUpperCase() }
// upperCasedName() // 直接報錯,由於delegate指向的對象沒有name屬性
upperCasedName.delegate = p
println(upperCasedName()) // delegate指向Person
upperCasedName.delegate = t
println(upperCasedName()) // delegate指向Thing
複製代碼

策略調整

閉包的委託是透明的,即便咱們沒有顯式地使用 delegate. 它也是能夠正常工做的。例如:

//name = 'jerry' // 若是定義了一個name,下面的閉包會打印JERRY
// 不顯式使用 delegate.
def upperCasedName2 = {
    println(name.toUpperCase()) // 打印TOM
    // 返回owner對象
    owner
}
// 直接賦值delegate
upperCasedName2.delegate = p
// 正常工做
println(upperCasedName2()) // 打印owner對象,指向的是groovy腳本
println(upperCasedName2() instanceof GroovyObject) // 實際上是GroovyObject類實例
複製代碼

這裏 name 屬性之因此能被正確的解析並執行都歸功於 delegate 對象,由於閉包優先使用 owner 對象來查找 name 屬性,發現沒有該屬性。而後再使用 delegate 對象,此時它指向了 p 擁有 name 屬性,因此方法獲得執行。

實際上,閉包的委託策略有多個:

  • Closure.OWNER_FIRST :默認策略。若是 owner 對象中有須要的屬性或方法,則優先執行;不然將使用 delegate 對象。在上例中,若是在閉包外面(腳本中)聲明瞭一個 name 屬性,則閉包中執行的 name 就是該屬性了,而輪不到 delegate 指向的對象。

  • Closure.DELEGATE_FIRST :delegate 優先策略,和默認策略邏輯相反,優先找 delegate 而不是 owner。

  • Closure.OWNER_ONLY :只使用 owner 策略,會忽略 delegate。

  • Closure.DELEGATE_ONLY :只使用 delegate 策略,會忽略 owner。

  • Closure.TO_SELF :可供須要高級元編程技術並但願實現自定義解析策略的開發人員使用。解決方案不會在 owner 或 delegate 上進行,而只能在封閉類自己上進行。 只有當你實現本身的 Closure 子類時,這個策略纔是有意義的。

那麼,怎麼調整委託策略呢?只須要賦值閉包的 resolveStrategy 屬性便可。

class Person2 {
    String name
    int age
    def fetchAge = { age }
}
class Thing2 {
    String name
}

def p2 = new Person2(name:'Jessica', age:42)
def t2 = new Thing2(name:'Printer')
def cl = p2.fetchAge
cl.delegate = p2
assert cl() == 42 // owner裏有age
cl.delegate = t2
assert cl() == 42 // 仍是找的owner,由於默認策略是owner優先
cl.resolveStrategy = Closure.DELEGATE_ONLY //切換爲只使用delegate
cl.delegate = p2 
assert cl() == 42 // delegate指向p2,有age屬性
cl.delegate = t2 
try {
    cl() // delegate指向t2,但t2中沒有age屬性,會拋出異常
    assert false
} catch (MissingPropertyException ex) {
    // "age" is not defined on the delegate
}
複製代碼

動態閉包

所謂動態閉包就是在代碼邏輯中判斷傳入的 Closure 是否知足某些要求,好比:Closure 是否存在,Closure 的參數數量和類型是否符合要求等,從而增長代碼的靈活性。

舉例說明:

判斷方法執行時是否傳入了閉包參數。若是傳了,則執行閉包邏輯;不然執行默認邏輯。

def say(closure) {
    if (closure) { // 判斷是否傳入了一個閉包
        closure()
    } else {
        println('say nothing')
    }
}

say()
say() {
    println('i\'m hungry')
}
複製代碼

判斷傳入的閉包的參數個數是否爲 2 個。

def compute(amount, Closure computer) {
    total = 0
    // 判斷閉包的參數個數是否爲2
    if (computer.maximumNumberOfParameters == 2) {
        total = computer(amount, 6)
    } else {
        total = computer(amount)
    }

    println("total is $total")
}

compute(100) {
    it * 8 // 800
}

compute(100) { // 600
    amount, weight ->
        amount * weight
}
複製代碼

咱們也能夠對閉包的參數類型作要求。

def execute(Closure closure) {
    println("params count: ${closure.maximumNumberOfParameters}")

    // 判斷delegate
    if (closure.delegate != closure.owner) {
        println('I have a custom delegate')
    }

    // 遍歷閉包的參數類型
    for (paramType in closure.parameterTypes) {
        if (paramType.name == 'java.util.Date') {
            println('Force today')
        } else {
            println(paramType.name)
        }

    }
}

execute {} // 一個Object類型參數
execute { it } // 一個默認參數,Object類型
execute { -> } // 沒有參數
execute { String value -> println(value) } // 一個String類型參數
execute {int a, Date b -> println('Two params')} // 兩個參數,int類型和Date類型

class A {
    String name
}
Closure closure = { println(it) }
closure.delegate = new A(name: 'Tom')
execute(closure)
複製代碼

動態的檢查閉包就可讓方法邏輯更加豐富,更加靈活了。

閉包的特點用法

前文中詳細介紹了 Groovy 閉包的基本概念、定義方式、基本使用以及重要的委託策略等,已經初步體現了閉包的強大。閉包做爲 Groovy 最重要的特點之一,它還有哪些有意思的特性呢?

在 GStrings 中使用

前面的例子中,在字符串裏用到了不少帶括號的寫法,好比:"My name is ${name}" ,這裏面是閉包嘛?咱們先來看一個例子。

def x = 1
// 這裏的 ${x} 並非閉包,而是一個表達式
def gs = "x = ${x}" // gs的值已經肯定了
assert gs == 'x = 1'

x = 2
//assert gs == 'x = 2' // gs仍是 x = 1
println(gs)
複製代碼

很遺憾,這種寫法並非閉包,而只是一個表達式:$x,它的值在 GString 建立時就已經肯定了。因此,當 x 賦值爲 2 時,gs 的值並無改變。

那如何實現 GString 中閉包的懶加載呢?就必須使用 ${-> x} 這種格式了,即顯式聲明 -> ,這樣在 GString 裏纔會是一個閉包。

def y = 1
// 這裏的 ${-> y} 纔是一個閉包
def gsy = "y = ${-> y}"
assert gsy == 'y = 1'

y = 2
assert gsy == 'y = 2' // 會從新使用y值, y = 2
複製代碼

其實,GString 只會延遲計算 toString 的表示,即只針對值的變化,而沒法做用於引用的變化。

// 定義一個類,包含toString方法
class Dog {
    String name
    String toString() { name }
}
def sam = new Dog(name:'Sam')
def lucy = new Dog(name:'Lucy')
def p = sam // 給p賦值
def gsDog = "Name: ${p}" // 此時gsDog的值已經肯定了
assert gsDog == 'Name: Sam'
p = lucy // 雖然p的引用發生了變化,但對gsDog無效
assert gsDog == 'Name: Sam'
sam.name = 'Lucy' // name的值發生了變化,會影響gsDog的值
assert gsDog == 'Name: Lucy'

// 若是改爲閉包,就會針對引用有效
def sam2 = new Dog(name: 'Sam2')
def lucy2 = new Dog(name: 'Lucy2')
def q = sam2
def gsDog2 = "Name: ${-> q}" // 使用閉包
assert gsDog2 == 'Name: Sam2'
q = lucy2 // q的引用發生了變化,對gsDog依然有效
assert gsDog2 == 'Name: Lucy2'
複製代碼

閉包的強轉

Groovy 中閉包是能夠強轉成 SAM 類型的。所謂的 SAM 類型是隻包含一個抽象方法的接口或者抽象類。咱們能夠經過 as 關鍵字實現強轉。

// SAM類型的接口和抽象類
interface Predicate<T> {
    boolean accept(T obj)
}

abstract class Greeter {
    abstract String getName()
    void greet() {
        println "Hello, $name"
    }
}

// 使用as關鍵字強轉
Predicate filter = { it.contains 'G' } as Predicate
assert filter.accept('Groovy') == true

Greeter greeter = { 'Groovy' } as Greeter
greeter.greet()
println(greeter.getName())

// 2.2.0以後的版本能夠省略as關鍵字
Predicate filter2 = { it.contains 'G' }
assert filter2.accept('Groovy') == true

Greeter greeter2 = { 'Groovy' }
greeter2.greet()
複製代碼

Groovy 的 2.2.0 版本以上能夠省略 as 關鍵字。

其實,除了 SAM 類型以外,閉包是能夠強制轉換爲任意類型的,特別是接口。

// 閉包能夠強制轉成任意類,特別是接口
//interface FooBar {
// int foo()
// void bar()
//}

class FooBar {
    int foo() { 1 }
    void bar() { println 'barn' }
}

def impl = { println 'ok'; 123 } as FooBar

assert impl.foo() == 123 // true
impl.bar() // 打印 ok
複製代碼

Curring

Curring 的中文是柯里化,屬於函數式編程的一種概念。但 Groovy 中的 Curring 並不符合真正的 Curring 概念,這是由於 Groovy 中的閉包有着不一樣的做用域規則。在 Groovy 中使用 Curring 操做能夠給 Closure 的參數列表中其中一個或多個參數進行賦值,而後會返回一個新的 Closure。

def nCopies = { int n, String str -> str*n }
def twice = nCopies.curry(2) // 最左邊的參數賦值
assert twice('bla') == 'blabla'
assert twice('bla') == nCopies(2, 'bla')
def blah = nCopies.rcurry('bla') // 最右邊的參數賦值
assert blah(2) == 'blabla'
assert blah(2) == nCopies(2, 'bla')

def volume = { double l, double w, double h -> l*w*h }
def fixedWidthVolume = volume.ncurry(1, 2d) // 指定某個位置的參數進行賦值,這裏指定第二個參數
assert volume(3d, 2d, 4d) == fixedWidthVolume(3d, 4d)
def fixedWidthAndHeight = volume.ncurry(1, 2d, 4d) // 第二個參數以後的參數
assert volume(3d, 2d, 4d) == fixedWidthAndHeight(3d)

def oneCurring = { item -> println(item)} // 支持一個參數的狀況
def oneCurring2 = oneCurring.curry("hello")
oneCurring2()

def funCurring = {int a, int b, int c, int d -> a * b * c * d}
def funCurring2 = funCurring.ncurry(1, 2, 3)
assert funCurring(1, 2, 3, 4) == funCurring2(1, 4)
複製代碼

Memoization

Groovy 的閉包擁有記憶化的能力,能夠理解爲緩存調用閉包的返回結果。這個特性能給一些須要進行重複計算的遞歸算法提供幫助,典型的例子是斐波那契數列。

def fib
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }
// 某些值會被重複計算
assert fib(15) == 610 // slow!
複製代碼

計算 fib(15) 時會要求先計算 fib(14) 和 fib(13) ,計算 fib(14) 時,又要先計算 fib(13) 和 fib(12) 。顯然 fib(13) 被重複計算了。使用閉包的 memoize 方法就能夠避免這些重複計算。

fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoize()
assert fib(25) == 75025 // fast!
複製代碼

除了 memoize 方法外,閉包還提供了幾個方法能夠控制緩存的數量,這些方法使用的是 LRU 策略。

  • memoizeAtMost :能夠設置最多緩存 n 個值,會生成一個新的 Closure;
  • memoizeAtLeast :能夠設置最少緩存 n 個值,會生成一個新的 Closure;
  • memoizeBetween :能夠設置緩存的數量範圍,會生成一個新的 Closure;
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoizeAtMost(8)
assert fib(25) == 75025 // fast!
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoizeAtLeast(3)
assert fib(25) == 75025 // fast!
fib = { long n -> n < 2 ? n : fib(n - 1) + fib(n - 2) }.memoizeBetween(3, 8)
assert fib(25) == 75025 // fast!
複製代碼

須要注意的是,閉包的緩存是使用參數的實際值工做的。這意味着若是是使用除基本類型或者包裹類型以外的其餘內容進行記憶化,就得額外當心了。

Trampoline

Trampoline 特性是專門爲遞歸調用提供的。使用遞歸時,最大的隱患是堆棧限制,若是遞歸深度過深,容易引起 StackOverflowException 異常。閉包的 Trampoline 特性可以幫助遞歸方法規避這個異常(尾遞歸)。

當咱們使用 trampoline() 方法時,會將當前的 Closure 包裝成 TrampolineClosure 。調用時就會返回 TrampolineClosure 的實例,並調用 call 方法,原閉包的邏輯就會被執行。若是有遞歸調用,則 call 方法返回的是 TrampolineClosure 另外一個實例,從而又會觸發原閉包的邏輯。以此不斷循環,直到返回非 TrampolineClosure 的值纔會結束。這樣就將原來的遞歸堆棧調用轉換成了方法的連續調用,從而避免堆棧溢出。

def factorial
factorial = { int n, def accu = 1G ->
    if (n < 2) return accu
    factorial.trampoline(n - 1, n * accu)
}
factorial = factorial.trampoline() // 會轉成 TrampolineClosure

assert factorial(1) == 1
assert factorial(3) == 1 * 2 * 3
assert factorial(1000) // == 402387260.. plus another 2560 digits
複製代碼

Composition

閉包的組合相對其餘特性來講簡單明瞭,能夠理解爲方法的組合調用。好比:有函數 f 和 g ,能夠構成一個新的函數 h = f( g( ) ) 或者 h = g( f( ) ) 等。舉個例子就明白了:

def plus2  = { it + 2 }
def times3 = { it * 3 }

// 先計算箭頭的尾部閉包,再將其值傳入箭頭的頭部閉包中進行計算
def times3plus2 = plus2 << times3 // plus2(times3())
assert times3plus2(3) == 11
assert times3plus2(4) == plus2(times3(4))

def plus2times3 = times3 << plus2 // times3(plus2())
assert plus2times3(3) == 15
assert plus2times3(5) == times3(plus2(5))

// reverse composition
def times3plus22 = times3 >> plus2
assert times3plus2(3) == times3plus22(3) // plus2(times3())
println(times3plus22(2)) // 8
複製代碼

總結

本文介紹了 Groovy 中的閉包,詳細說明了閉包的定義規則、委託機制以及特點使用。這些內容已經體現出了閉包的魅力,但遠遠不僅如此。閉包真正強大的地方是它在 DSL 領域的做用。有興趣的同窗能夠自行預習,在後續的 Gradle 文章中會用到 DSL 。

參考資料

  1. 柯里化函數
  2. 真正意義上的閉包
  3. Groovy 官方網站
  4. 尾遞歸
  5. LRU 算法
  6. Lambda 表達式
  7. 《Groovy程序設計》
相關文章
相關標籤/搜索