java閉包詳解

語言: CN / TW / HK

一、為什麼需要閉包

  • 閉包的價值在於可以作為函式物件或者匿名函式,持有上下文資料,作為第一級物件進行傳遞和儲存。
  • 閉包廣泛用於回撥函式、函數語言程式設計中。

二、閉包的概念

  • 閉包是一個可呼叫的物件,它記錄了一些資訊,這些資訊來自於建立他的作用域,用過這個定義 可以看出內部類是面向物件的閉包 因為他不僅包含外圍類物件的資訊 還自動擁有一個指向此外圍類物件的引用 在此作用域內 內部類有權操作所有的成員 包括private成員;interfaceIncrementable。 (出自 java程式設計思想)
  • java中的內部類 採用 ”介面 + 內部類“ 實現。

三、java 內部類的幾種表現形式

內部類

     在JAVA中,內部類可以訪問到外圍類的變數、方法或者其它內部類等所有成員,即使它被定義成private了,但是外部類不能訪問內部類中的變數。這樣通過內部類就可以提供一種程式碼隱藏和程式碼組織的機制,並且這些被組織的程式碼段還可以自由地訪 問到包含該內部類的外圍上下文環境。

public class DemoClass{
private int length =0;

private class InnerClass implements ILog
{
    @Override
    public void Write(String message) {          
        System.out.println("DemoClass.InnerClass:" + length);
    }
}

public ILog logger() {
    return new InnerClass();
}

public static void main(String[] args){
    DemoClass demoClass = new DemoClass();
    demoClass.logger().Write("abc");

    //new
    DemoClass dc = new DemoClass();
    InnerClass ic = dc.logger();
    ic.Write("abcde");
}
}

從上可見,InnerClass是定義在DemoClass內部的一個內部類,而且InnerClass還可以是Private。

  • 如何通過this顯式引用外圍類的變數? * 通過此格式進行引用:{外圍類名}.this.{變數名稱}。如: DemoClass. .length = message.length();

區域性內部類

     區域性內部類是指在方法的作用域內定義的的內部類。

public class DemoClass2 {
    private int length =0;

    public ILog logger() {
        //在方法體的作用域中定義此區域性內部類
        class InnerClass implements ILog
        {
            @Override
            public void Write(String message) {
                length = message.length();
                System.out.println("DemoClass2.InnerClass:" + length);
            }
        }
        return new InnerClass();
    }
}

因為InnerClass類是定義在logger()方法體之內,所以InnerClass類在方法的外圍是不可見的。

匿名內部類

    匿名內部類就是匿名、沒有名字的內部類,通過匿名內部類可以更加簡潔的建立一個內部類。

public class DemoClass3 {
    private int length =0;

    public ILog logger() {
       return new ILog() {
            @Override
            public void Write(String message) {
                  length = message.length();
                  System.out.println("DemoClass3.AnonymousClass:" + length);
            }
       };
    }
}

由此可見,要建立一個匿名內部類,可以new關鍵字來建立。

  • 格式:new 介面名稱(){}

  • 格式:new 介面名稱(args…){}

final關鍵字

  閉包所繫結的本地變數必須使用final修飾符,以表示為一個恆定不變的資料,建立後不能被更改。

public class DemoClass4 {
    private int length =0;

    public ILog logger(int level) {//final int level
        //final
        final int logLevel = level+1;

        switch(level)
        {
            case 1:
                return new ILog() {
                    @Override
                    public void Write(String message) {
                        length = message.length();
                        System.out.println("DemoClass4.AnonymousClass:InfoLog " + length);
                        System.out.println(logLevel);
                    }
                };    
            default:
            return new ILog() {
                @Override
                public void Write(String message) {
                    length = message.length();
                    System.out.println("DemoClass4.AnonymousClass:ErrorLog " + length);
                    System.out.println(logLevel);
                }
            };

        }
    }

    public static void main(String[] args){
        DemoClass4 demoClass4 = new DemoClass4();
        demoClass4.logger(1).Write("abcefghi");
    }

}

從例子中可以看到,logger方法接受了一個level引數,以表示要寫的日誌等級,這個level引數如果直接賦給內部類中使用,會導致編譯時錯誤,提示level引數必須為final,這種機制防止了在閉包共享中變數取值錯誤的問題。解決方法可以像例子一樣在方法體內定義一下新的區域性變數,標記為final,然後把引數level賦值給它:

  final int logLevel = level ;
  //或者直接引數中新增一個final修飾符:
  public ILog logger(final int level {

例項初始化

    匿名類的例項初始化相當於構造器的作用,但不能過載。

public ILog logger(final int level) throws Exception {

        return new ILog() {
            {
                //例項初始化,不能過載 
                if(level !=1)
                    throw new Exception("日誌等級不正確!");
            }

            @Override
            public void Write(String message) {
                length = message.length();
                System.out.println("DemoClass5.AnonymousClass:" + length);
            }
        };
    }

匿名內部類的例項初始化工作可以通過符號 {…} 來標記,可以在匿名內部類例項化時進行一些初始化的工作,但是因為匿名內部類沒有名稱,所以不能進行過載,如果必須進行過載,只能定義成命名的內部類。

四、閉包的問題

  1. 讓某些物件的生命週期加長。
    讓自由變數的生命週期變長,延長至回撥函式執行完畢。
  2. 閉包共享。
    final關鍵字
interface Action
{
    void Run();
}

public class ShareClosure {

    List<Action> list = new ArrayList<Action>();

    public void Input()
    {
        for(int i=0;i<10;i++)
        {
            final int copy = i;
            list.add(new Action() {    
                @Override
                public void Run() {
                    System.out.println(copy);
                }
            });
        }
    }

    public void Output()
    {
        for(Action a : list){a.Run();}
    }

    public static void main(String[] args) {
        ShareClosure sc = new ShareClosure();
        sc.Input();
        sc.Output();

    }

}

這個例子建立一個介面列表List ,先向列表中建立 i 個匿名內部類new Action(),然後通過for遍歷讀出。 因為 i 變數在各個匿名內部類中使用,這裡產生了閉包共享,java編譯器會強制要求傳入匿名內部類中的變數新增final 關鍵字,所以這裡final int copy = i;需要做一個記憶體拷貝,否則編譯不過。