java閉包和回撥淺析

Joerrot發表於2018-07-19

按照概念,閉包(Closure)是一種能被呼叫的物件,它儲存了建立它的作用域的資訊。

我們先來看下面的例子:

//Programmer.java

interface inter{
    void work();
}
public class Programmer{ //並沒有實現介面inter
    private String name;
    public Programmer(){}
    public Programmer(String name){
        this.name = name;
    }
   ...//此處省略了name屬性的setter和getter方法
    public void work(){
        System.out.println(name+"在敲程式碼");
    }
}
//Teacher.java

public class Teacher extends Programmer implements inter{
    public void work(){
        System.out.println(super.name+"在講課");
    }
}
//Test.java

public class Test{
    public static void main(String[] args){
        Programmer p = new Programmer("老朱");
        p = new Teacher();
        p.work(); 
    }
}

此時輸出的只能是:老朱在講課。

但是實際上我們需要通過 t 來呼叫相關函式來輸出:老朱在敲程式碼。

現在,我們考慮一種情況:

我們新建一個 TestOther.java檔案,類TestOther繼承類Programmer:

在類TestOther內部定義一個內部類(我們規定,該內部類只由該外部類所屬,用private修飾),

現在介面該由誰來實現呢?

我們最後都是要到main函式中 new一個TestOther物件,然後利用物件來呼叫Programmer中的work()函式以及TestOther中的work()函式的。現在TestOther已經繼承了Programmer類,就不能在TestOther中直接定義一個work()函式了,所以介面不能由該類來實現。那我們就直接定義一個teach()函式,功能與TestOther中的work()函式相同。

我們知道,非靜態內部類的方法可以訪問外部類的成員。

所以,如果我們定義一個Closure內部類,然後在該類的內部定義一個work()函式(介面由內部類來實現),函式體用來回撥外部類的成員teach();

這樣 ,在另一個.java檔案TestAll.java的main函式之中,我們通過TestOther的物件,可以直接呼叫work()函式(繼承過來的,也就是父類Programmer的work()函式),就會輸出:**在敲程式碼;

現在,如果讓TestOther中的teach()函式變為public函式,事情就簡單多了,直接利用上面的物件來呼叫即可輸出:**在講課。

但是為了體現java程式設計的靈活性,(在不改變teach()函式的private屬性的情況下也不得不這麼做):

現在,非靜態內部類Closure內部的work()函式也回撥了teach()函式,而且,考慮到內部類是private修飾的,所以不能在外部類外部進行例項化內部類物件來呼叫它定義的介面inter的work()函式。

所以我們在外部類中再定義一個public函式getCallbackReference(),來返回new Closure();即可,那麼根據向上轉型的引數統一化,我們利用介面inter作為函式的返回值型別。

這樣,我們就可以在TestAll.java檔案的main函式中利用TestOther的物件來呼叫public函式getCallbackReference(),利用其返回值(例項化內部類物件)來呼叫非靜態內部類的work()函式,這樣通過回撥teach()函式,就可以輸出:**在講課。

下面給出相關程式碼:

//TestOther.java

public class TestOther extends Programmer{
    public TestOther(){}
    public TestOther(String name){
        super(name);
    }
/*
定義一個private函式,功能也是輸出:**在講課
*/
    private void teach(){
        System.out.println(getName()+"在講課");
    }
/*
非靜態內部類回撥外部類成員實現work()方法,非靜態內部類的作用僅僅是向客戶類
提供過一個回撥外部類的途徑,但該類為private修飾,不能在外部直接例項化物件,
所以需要再定義下面的getCallbackReference()函式
*/
    private class Closure implements inter{
        public void work(){
                teach();
        } 
    }
/*
定義一個可以被外部呼叫的public函式,用向上轉型的規範返回非靜態內部類的例項化物件
*/
    public inter getCallbackReference(){
        return new Closure();
    }
}
//TestAll.java

public class TestAll{
    public static void main(String[] args){
        TestOther tp = new TestOther(""老朱);
        tp.work();  //直接呼叫TestOther類從Programmer類繼承到的work()方法
        //表面上呼叫的是Closure的work()方法,實際上是回撥TestOther的teach()方法
        tp.getCallbackReference().work();
    }
}

實際上,java並不能顯示地支援閉包,但是對於非靜態內部類而言,它不僅記錄了其外部類地詳細資訊,還保留了一個建立非靜態內部類物件的引用,並且可以直接呼叫外部類的private成員,因此,可以把非靜態內部類當成物件導向領域的閉包。

通過這種仿閉包的非靜態內部類,可以很方便地實現回撥功能,回撥就是某個方法一旦獲得了內部類物件的引用後,就可以在合適的時候反過來呼叫外部類例項的方法。所謂回撥,就是允許客戶類(main函式所在的類)通過內部類引用來呼叫其外部類的方法。這是一種非常靈活的功能。

下面給出另一個相似的例子:

interface inter{
    void fun();
}

class A implements inter
{
    private int i = 0;
    public void fun()
    {
        i++;
        System.out.println(i);
    }
}

class B
{
    public void fun()
    { 
        System.out.println("B、fun()"); 
    }
    static void fB(B b)
    { 
        b.fun(); 
    }
}

class C extends B{
    private int i = 0;
    public void fun()
    {
        super.fun();
        i++;
        System.out.println(i);
    }
    private class Closure implements inter
    {
        public void fun()
        {
            C.this.fun();
        }
    }
    public inter getCallbackReference()
    {
        return new Closure();
    }
}

class M
{
    private inter in;
    M(inter i){ 
        in = i; 
    }
    void funM(){ 
       in.fun(); 
    }
}

public class TestDemo {
    public static void main(String[] args){
        A a = new A();
        C c = new C();
        B.fB(c);
        M m1 = new M(a);
        M m2 = new M(c.getCallbackReference());
        m1.funM(); 
        m1.funM();
        m2.funM();
        m2.funM();
    }

}

輸出結果是:

B、fun()
1
1
2
B、fun()
2
B、fun()
3

首先,A簡單的實現了介面inter與相關方法,在這裡起到一個對比的作用而已。

然後定義一個B類同樣實現了一個fun()方法但是這個與介面中的increment()沒有任何關係,因為這個類自己實現的,並沒有實現這個介面,而靜態方法fB()也只是為了測試一下B類自己的fun()方法。

而C類繼承自B類。同樣寫了一個fun()方法,覆蓋了父類的fun()方法,但是函式內部還是呼叫了父類的fun()方法。

接著,在C類中定義了一個private修飾的非靜態內部類Closure(也就是閉包的具體實現了)。

內部類實現了介面inter並且直接呼叫外部類的方法作為具體的實現。

內部類實現inter介面很關鍵,這樣就給外部留下了一個通道,能夠接受這個內部類。

最後類C的後面留下了一個鉤子,即getCallbackReference()方法,它返回一個內部類的物件,實現了內部與外部的連結,同時有保證了內部類的安全,因為只有Callee2的物件可以訪問與呼叫這個內部類的方法,而其他的類都無權訪問,即使是基類介面物件。

而後面的定義的M類,起到的是喚醒作用,利用有參建構函式,通過接受不同的介面物件傳入形參,實現不同的操作(向上轉型規範)。

但還有一個作用是等待接受一個內部類物件,來產生回撥,因為一旦傳入的是具有內部類的類C例項化物件呼叫的getCallbackReference()函式,就可以實現回撥作用。現在大家再回頭看一下輸出就能夠明白了。

假裝你回頭看了,在main()方法中,首先是建立物件與宣告,然後是呼叫了B類的靜態方法fB(),(B是C的父類)傳入的是一個C的例項化物件,此時無法觸發回撥,只是正常的輸出(看起來很像回撥,實際上只是執行了類B的靜態成員fB()而已)

然後,在對C的初始化時傳入的是一個Closure物件從而產生了回撥。

以上就是java的閉包與回撥機制,結合後面的內容會有更多意想不到的作用。

相關文章