閉包與內部類

即便那總是過去發表於2020-05-19

閉包與內部類

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

內部類

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

Java中的內部類

在java中,內部類有:

  • 成員內部類
  • 靜態內部類
  • 區域性內部類
  • 匿名內部類

成員內部類

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;
        }
    }
}

拿以上程式碼舉例,成員內部類可以訪問到外部類中的所有欄位、方法,包括私有。
內部類的實現均是通過編譯器構造位元組碼實現的。上述類經過編譯產生的類大概如下

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的對外部物件的引用。

靜態內部類
靜態內部類並不具有內部類和外部類之間的依賴關係,靜態內部類和在一個檔案中寫兩個類沒啥卻別,一般用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;
            }
        }
    }
}

上述程式碼構造出的類如下:

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的操作,就是這個原理。

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

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

相關文章