闭包与内部类

  • A+

闭包与内部类

中英文社区中,比较常见的对闭包的定义是
引用了自由变量的一段代码或函数,被引用的自由变量和函数(一段代码)共同存在,即使离开了创造它的环境

内部类

按照我的理解,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

90DIR-CMD