閉包與內部類

閉包與內部類

中英文社區中,比較常見的對閉包的定義是
引用了自由變量的一段代碼或函數,被引用的自由變量和函數(一段代碼)共同存在,即便離開了創造它的環境java

內部類

按照個人理解,scala/java中雖然並不存在語法級地支持或是定義,對於閉包而言,一些概念和閉包的概念一致。通常理解scala中的一些概念,我會傾向於從Java開始。git

Java中的內部類

在java中,內部類有:github

  • 成員內部類
  • 靜態內部類
  • 局部內部類
  • 匿名內部類

成員內部類閉包

class Outer1{
    private int a1;
    private static int s1;
    void f1() {

    }

    class Inner1{
        int a2;
        void f2(){
            //access outer's field,function
            int b=a1; //能夠直接引用或是Outer1.this.a1;
            Outer1.this.f1();
            int c=Outer1.s1;
        }
    }
}

拿以上代碼舉例,成員內部類能夠訪問到外部類中的全部字段、方法,包括私有。
內部類的實現均是經過編譯器構造字節碼實現的。上述類通過編譯產生的類大概以下app

class Outer1{
    private int a1;
    private static int s1;
    void f1() {

    }
    static int access$000(Outer1 outer){
        return outer.a1;
    }
    int access$100(){
        return a1;
    }
    
}
class Inner1{
    int a1;
    final Outer1 this$0;
    Inner1(Outer1 outer){
        this.this$0=outer;
    }
    void f2(){
            int b=Outer1.access$000(this$0);
            this$0.f1();
            int c=Outer1.access$100();
    }
}

能夠看到,在外部類中添加了相應的方法,給內部類調用來達到訪問外部類的private成員或是方法的目的。在內部類中,會添加一個this$0的對外部對象的引用。ide

靜態內部類
靜態內部類並不具備內部類和外部類之間的依賴關係,靜態內部類和在一個文件中寫兩個類沒啥卻別,通常用privat static內部類來隱藏實現。函數

class SomeInterface{
    void function();
}
class SomeInterfaceFactory{
    private static class SomeInterfaceImpl implements SomeInterface{

    }
    static newInstance(){
        return new SomeInterfaceImpl()
    }
}

局部內部類
內部類能夠寫在函數中,除了外部類的變量和方法外,內部類還能夠訪問到函數中的局部變量.測試

class Outer3{
    int a1;
    void function(){
        int used=0;
        int notUsed=-1;
        class Inner3{
            void f2(){
                int t1=used;
                int t2=a1;
            }
        }
    }
}

上述代碼構造出的類以下:this

class Outer3{
    int a1;
    void function(){
        int used=0;
        int notUsed=-1;
    }
}
class Inner3{
    final int val$used; //從這裏看出不能對外部變量賦值
    final Outer3 this$0;

    Inner3(Outer3 outer,int a){
        this.this$0=outer;
        this.val$used=a
    }

    void f2(){
        int t1=val$used;
        int t2=this$0.a1;
    }

}

從上面能夠看出,局部內部類除了像成員內部類那樣添加了外部對象的引用,還添加了對引用到的局部變量的引用,而且這些屬性會經過構造函數進行初始化。
此外,在Inner3的f2中,不能執行相似used=10的操做,是由於這些引用是final的,固然,對於對象類型,對象內部仍是能夠修改的,scala中的局部內部類能夠更改執行相似used=10的操做,就是這個原理。spa

匿名內部類
匿名內部類和局部內部類沒有太大區別,只是生成的類的類名不含用戶的標識。

class Outer4{
    int a1;
    void function(){
        int used=0;
        int notUsed=-1;
        Runnable t=new Runnable() {
            @Override
            public void run() {
                int t1=used;
                int t2=a1;
            }
        };
    }
}

上述代碼構造出的類以下:

class Outer4{
    int a1;
    void function(){
        int used=0;
        int notUsed=-1;
        Runnable t=new Outer4$1();
    }
}
class Outer4$1 implements java.lang.Runnable{
    final int val$used; 
    final Outer4 this$0;
    public void run(){
        //...
    }
}

總結
除靜態內部類外,java編譯器會作如下事情:

  1. 在內部類添加字段,類型是外部類,即內部對象持有外部類對象的引用。
  2. 當內部類訪問到外部類的private的屬性時,編譯器會在外部類中添加相應的getter/setter,內部類用它們對private屬性訪問。
  3. 對局部內部類和匿名內部類而言,使用到的局部變量會轉換爲不可變的成員變量。

Scala中的內部類

scala中,內部類的實現與java基本一致,其中函數實現相似實現AbstractFunction接口的的匿名內部類。
成員內部類
成員內部類與java基本一致,對private屬性的處理稍微有些不一樣,在scala中,其實全部成員變量均是private的,編譯器自動爲其添加相應的getter/setter,所以若是內部類訪問了外部類的私有屬性,則編譯器只需調整相應的getter的訪問權限。

局部內部類
局部內部類也與java基本一致,只是局部內部類中能夠修改外部的局部變量。其實現原理也很簡單,在局部變量外再包一層,如int改成IntRef,好比

final int[] data=new int[1]
data[0]=1 //set
int tmp=data[0] //get

匿名內部類
scala中,除了像java那樣定義匿名內部類外,函數實現也是匿名內部類實現。如函數

class Outer4{
    private val a1=-1
    val f1: Int => Int = (a: Int) => a + 1
    val f2: Int => Int = (a: Int) => a + a1
}

會生成相似以下匿名內部類

//對應f1
public final class Outer4$$anonfun$3 extends scala.runtime.AbstractFunction1$mcII$sp implements scala.Serializable {
    public static final long serialVersionUID=0L;
    public final int apply(int){
        //計算邏輯
    }
    public Outer4$$anonfun$3(Outer4 outer){
        //...
    }
}
//對應f1
public final class Outer4$$anonfun$3 extends scala.runtime.AbstractFunction1$mcII$sp implements scala.Serializable {
    public static final long serialVersionUID=0L;
    private final Outer4 $outer;

    public final int apply(int){
        //計算邏輯
    }
    public Outer4$$anonfun$3(Outer4 outer){
        //...
    }
}

從上面的例子來看,最大的不一樣是外部對象引用的不一樣,在某些狀況下一個匿名內部類可能僅僅是'匿名類',經過測試驗證,發現僅在如下狀況下內部類會添加對外部類的引用:

  1. 內部類訪問了外部類的屬性
  2. 內部類訪問了外部類的方法

還有一個問題,若是在函數A內部定義了函數B,函數B訪問了函數A的局部變量,則函數B的匿名內部類會添加函數A的匿名內部類的引用嗎?

class Outer4{
    val a1=-1
    val fa=()=>{
        val local=a1
        val fb=()=>{
            println(local)
        }
        val fc=()=>{
            println(a1)
        }
    }
}

答案是否認的。
在以上代碼中,fb不會持有外部對象即fa的引用,fb對local引用被看成局部變量處理,
這和上面java例子中Inner3對used變量的訪問一致。
fc會持有外部對象即fa的引用,這是由於fc訪問了a1,至關於fa.outer.a1.

總結

  1. scala中內部類的機制與java基本一致。
  2. scala藉助匿名內部類實現函數對象,匿名內部類只有訪問外部對象的方法或字段時纔會持有外部對象的引用。
  3. 2這一點想必是出於對函數或是閉包的支持,若是一個函數對象沒有引用上下文的變量或函數,就持上下文的引用,也就沒法造成閉包。所以從這個角度說,java匿名內部類或其餘內部類,不是真正意義上的閉包。

版本說明

輪子 版本
java 1.8
scala 2.11.12
spark 2.2.0

參考文檔

閉包
https://github.com/ColZer/DigAndBuried/blob/master/spark/function-closure-cleaner.md

相關文章
相關標籤/搜索