JAVA閉包

一、閉包的定義。

  有很多不同的人都對閉包過進行了定義,這裏收集了一些。

  # 是引用了自由變量函數。這個函數通常被定義在另一個外部函數中,並且引用了外部函數中的變量。 -- <<wikipedia>>

  # 是一個可調用的對象,它記錄了一些信息,這些信息來自於創建它的作用域。-- <<Java編程思想>>

  # 是一個匿名的代碼塊,可以接受參數,並返回一個返回值,也可以引用和使用在它周圍的,可見域中定義的變量。-- Groovy ['ɡru:vi]

  # 是一個表達式,它具有自由變量及邦定這些變量的上下文環境

  # 閉包允許你將一些行爲封裝,將它像一個對象一樣傳來遞去,而且它依然能夠訪問到原來第一次聲明時的上下文

  # 是指擁有多個變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。

  # 閉包是可以包含自由(未綁定)變量代碼塊;這些變量不是在這個代碼塊或者任何全局上下文中定義的,而是在定義代碼塊的環境中定義。

  在這些定義中都有一些關鍵字:變量、函數、上下文等,閉包在回調函數函數式編程Lambda表達式中有重要的應用,爲了更深刻的理解閉包,我們會試圖通過JavaScriptC#JAVA的代碼進行舉例,不過本次的重點還是通過JAVA如何這內部類來實現閉包,以及閉包的應用。

 

二、JavaScript中的閉包。

  在JavaScript中,閉包是通過函數的嵌套來實現,以下是一個簡單的例子:

\JSClosure\Closure1.htm
<script type="text/javascript">
function f1() {
var n = 99;
function f2() {
alert(n);
}
return f2();
}
f1();
</script>

這段代碼的特點:

1、函數f1()還回了函數f2()

2、函數f2()引用了f1()定義的局變量

正常來講,我們在外部是不能操作到f1()函數內部所定義的局部變量n,但是通過變通的方法,我們在f1()函數內部定義了一個新的函數f2(),通過f2()輸出其外圍函數的局部變量n,f2()是f1()的內部函數,對於f2()來說其外圍函數所定義的變量、函數等上下文是可以被內部函數所訪問到的;最後在f1()函數中再調用f2()以在f1()被調用時觸發對f2()的調用,從而把局部變量輸出。 我們對照一下閉包的定義:"引用了自由變量的函數",這裏的n就是定義中的自由變量,而函數f2()通過邦定自由變量n從而形式了一個閉包。

 

二、.NET中的閉包。

  在.NET中是通過delegate委託實現閉包的,在C#2.0時代可以通過匿名方法(函數)生成,在C#3.0時代建議使用Lambda生成,但是無論版本怎麼變化,其本質還是通過delegate實現,其它形式都是些語法糖。(Lambda表達式實質上還是上生成了匿名函數)。從本質上來講,最終於生成IL代碼後,delegate其實就是一個繼承了System.MulticastDelegate 或 System.Delegate的類。

這裏是一個匿名方法的例子:

 

\DelegateClosure\Program.cs
public static void TestDelegate(string url)
{
WebRequest request
= HttpWebRequest.Create(url);
request.BeginGetResponse(
delegate(IAsyncResult ar)
{
using (WebResponse response = request.EndGetResponse(ar))
{
Console.WriteLine(
"{0}: {1}", url, response.ContentLength);
}
},
null);
}

 

這個例子是通過WebRequest獲取某個url指定網頁的內容大小的示例程序,這裏的BeginGetResponse方法需要接收一個委託類型的變量。

delegate void AsyncCallback(IAsyncResult ar);

我們知道,delegate本質上最終於會生成一個類,而在這個委託對象內部分別邦定了request變量和url參數,在這個例子裏我們說這個匿名方法以及它所邦定的變量構成了一個閉包。

如果使用Lambda表達式,可以寫成這樣:

 

\DelegateClosure\Program.cs
public void TestLambda()
{
WebRequest request
= HttpWebRequest.Create(url);
request.BeginGetResponse(ar
=>
{
using (WebResponse response = request.EndGetResponse(ar))
{
Console.WriteLine(
"{0}: {1}", url, response.ContentLength);
}
},
null);
}

 

可以看到Lambda表達式的代碼更爲簡潔,但本質上它還是生成了匿名函數。

 

三、JAVA中的閉包。

  在JAVA中,閉包是通過“接口+內部類”實現,像C#的delegate一樣,JAVA的內部類也可以有匿名內部類。我們現在就來詳細認識一下JAVA內部類。

1、內部類。

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

這裏提供了一個例子展示這種機制:

 

/JavaClosure/src/innerclass/DemoClass1.java
public class DemoClass1 {
private int length =0;

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

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

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

//.new
DemoClass1 dc1 = new DemoClass1();
InnerClass ic
= dc1.new InnerClass();
ic.Write(
"abcde");
}
}

該例子的主要功能是實現一個寫日誌的ILog接口,但是該接口的類被定義在DemoClass1這個外圍類中了,而且這個InnerClass內部類還可以訪問其外圍類中的私有變量length。

1.1、.new

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

如何創建這個InnerClass的實例? 可以通過外圍類的實例進行創建,如:

DemoClass1 dc1 = new DemoClass1();
InnerClass ic
= dc1.new InnerClass();
ic.Write(
"abcde");

1.2、.this

 

  如何通過this顯式引用外圍類的變量?通過此格式進行引用:{外圍類名}.this.{變量名稱}。如:

  DemoClass1.this.length = message.length();

 

2、局部內部類。

  局部內部類是指在方法的作用域內定義的的內部類

 

/JavaClosure/src/innerclass/DemoClass2.java
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類在方法的外圍是不可見的。

 

3、匿名內部類。

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

 

/JavaClosure/src/innerclass/DemoClass3.java
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...){}

 

4、final關鍵字。

  閉包所綁定的本地變量必須使用final修飾符,以表示爲一個恆定不變的數據,創建後不能被更改。

 

/JavaClosure/src/innerclass/DemoClass4.java
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 {

 

5、實例初始化。

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

/JavaClosure/src/innerclass/DemoClass5.java
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);
}
};
}

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

 

四、爲什麼需要閉包。

  閉包的價值在於可以作爲函數對象或者匿名函數,持有上下文數據,作爲第一級對象進行傳遞和保存。閉包廣泛用於回調函數函數式編程中。

原生java沒有提供Lambda表達式,不過可以使用嘗試使用Scala的Lambda:

例子1:這個是閉包不?

scala> var add = (x: Int) => x +1
scala
> add(10)

例子2:

scala> var more = 1
scala
> var addMore = (x: Int) => x + more
scala
> addMore(10)

五、閉包的問題。

1、讓某些對象的生命週期加長。

  讓自由變量的生命週期變長,延長至回調函數執行完畢。

2、閉包共享。

  inal關鍵字

 

/JavaClosure/src/innerclass/ShareClosure.java
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<Action> ,先向列表中創建 i 個匿名內部類new Action(),然後通過for遍歷讀出。

因爲 i 變量在各個匿名內部類中使用,這裏產生了閉包共享,java編譯器會強制要求傳入匿名內部類中的變量添加final

關鍵字,所以這裏final int copy = i;需要做一個內存拷貝,否則編譯不過。(在c#中沒有強制要求會導致列有被遍歷時

始終會取 i 最大值,這是因爲延遲執行引起的)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章