关于闭包

先来看一个问题:

  • 为什么内部类(inner class)用到的外部变量只能是 final 类型?

例如以下代码中变量 x 为什么必须声明为 final

public class Closure {
  public static void main(String... args) throws InterruptedException {
    final int x = args.length;
    new Thread() {
      public void run() {
        System.out.println(x);
      }
    }.start();
  }
}

如果去掉 final 关键字,编译阶段就会报错:

Closure.java:6: local variable x is accessed from within inner class; needs to be declared final
        System.out.println(x);
                           ^
1 error

Java编程思想 中虽然提到了这个规则,但也没有说明为什么。

  • If you’re defining an anonymous inner class and want to use an object that’s defined outside the anonymous inner class, the compiler requires that the argument reference be final.

JLS 8.1.3 中对此的说明:

  • Any local variable, formal method parameter or exception handler parameter used but not declared in an inner class must be declared final. Any local variable, used but not declared in an inner class must be definitely assigned before the body of the inner class.*

但是为什么会有这样的语法规则呢?

其中这就是 Java 中的闭包(Closure),虽然它不是严格意义上的闭包。

什么是闭包

引用 wikipedia 的定义:

  • In programming languages, a closure (also lexical closure or function closure) is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.A closure—unlike a plain function pointer—allows a function to access those non-local variables even when invoked outside its immediate lexical scope.*

简而言之

  • 闭包就是一个带数据的函数,这个函数有一个函数环境,环境内包含了函数所用到的非本地变量。

下面以python代码为例,演示闭包的用法。

def maker(N):
    def action(X):
        return X \*\* N
    return action

maker 函数返回了一个嵌套函数(Nested Function)action,其中 action 用到了 maker 的变量 N

执行一下:

f = maker(2)
print(f)

结果为

<function maker.<locals>;.action at x7ff257b62950>*

可以看出 f 是函数 maker 的一个 local 函数 —- action f = maker(2) 返回之后,maker 的生命周期也结束了,按道理它的 local 变量N,也应该会被释放掉,但是当我们执行如下语句时

print(f(3))
print(f(4))

返回值为

9
16

也就是说,即使maker函数的生命周期已经结束,但是action仍然记住了它所用到的maker的局部变量N。

在这里,f就是一个闭包,它包含的不仅是action函数,还有action用到的数据 —- a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function

如果使用 lambda 表达式,上面的 maker 可以修改为

def maker(N):
	return lambda x: x ** N

以上就是 Python 中闭包的用法,但是以上用法在 Java 根本无法实现,因为 Java 是一个面向对象的语言,不可能返回一个函数,那么 Java 语言中对闭包又是如何处理的呢?

闭包跟对象的区别

《こーどの未来》中解释了两者的区别。

  • 「手続き(関数)とデータの一体化」というのは、オブジェクト指向のオブジェクトを形容するときによく用いられる表現です。オブジェクトはデータにメソッドという形で手続きが内包されているものですが、クロージャーは手続きに環境という形でデータが内包されています。つまり、オブジェクトとクロージャーは表裏一体と考えてもよいでしょう

稍微总结以下:

  • 对象是在数据的基础上,以方法的形式把函数包含在内。
  • 闭包是在函数的基础上,以环境的形式把数据包含在内。

Java 与闭包

通过 Python 语言的示例,可以比较容易得理解闭包与对象之间的区别,我们再来看一下文章开头提出的问题。

为什么内部类(inner class)用到的外部变量只能是final类型 ?

public class Closure {
  public static void main(String... args) throws InterruptedException {
    final int x = args.length;
    new Thread() {
      public void run() {
        System.out.println(x);
      }
    }.start();
  }
}

其实,编译器为 Thread() 匿名类自动添加了一个参数为 int 的构造函数,在新建 Thread 对象时,x 值被传入,成为 Thread 对象的数据。如此以来,编译器则没有必要像 python 那样为 inner class 维护一个 referencing environment

我们可以用一段代码把这个构造函数找出来。

import java.lang.reflect.Constructor;
public class Closure {
  public static void main(String... args) throws InterruptedException {
    final int x = args.length;
    new Thread() {
      public void run() {
        System.out.println(x);
        for (Constructor<?> cons : this.getClass().getDeclaredConstructors()) {
          StringBuilder sb = new StringBuilder();
          sb.append("constructor: ").append(cons.getName()).append("(");
          for (Class<?> param : cons.getParameterTypes()) {
            sb.append(param.getSimpleName()).append(", ");
          }
          if (sb.charAt(sb.length() - 1) == ' ') {
            sb.replace(sb.length() - 2, sb.length(), ")");
          } else {
            sb.append(')');
          }
          System.out.println(sb);
        }
      }
    }.start();
  }
}

输出结果是

0
constructor: Closure$1(int)
constructor: Closure$1(int) 即为编译器添加的构造方法。

x 就是通过这个构造函数拷贝到内部类 - new Thread() 当中,但是用到 x 的方法 run 并不会按照代码顺心立即被执行,如果在 new Thread() 之后的代码中改变了 x 的值,那么多 run 方法中用到的 x 就是一个过时(out-of-date)的 x。

同样的,如果run方法改变了 x 的值就破坏了 main 方法的数据流,我们希望内部类对使用者来说是透明,加上了 final 关键字就保证了 x 只在方法的 enclosing scope 是可变的,这样就不必为了追踪 x 的变化而必须搞清楚内部类的逻辑。

StackOverflow 上Jon Skeet 解释得非常清楚。

When you create an instance of an anonymous inner class, any variables which are used within that class have their values copied in via the autogenerated constructor. This avoids the compiler having to autogenerate various extra types to hold the logical state of the “local variables”, as for example the C# compiler does… (When C# captures a variable in an anonymous function, it really captures the variable - the closure can update the variable in a way which is seen by the main body of the method, and vice versa.) As the value has been copied into the instance of the anonymous inner class, it would look odd if the variable could be modified by the rest of the method - you could have code which appeared to be working with an out-of-date variable (because that’s effectively what would be happening… you’d be working with a copy taken at a different time). Likewise if you could make changes within the anonymous inner class, developers might expect those changes to be visible within the body of the enclosing method. Making the variable final removes all these possibilities - as the value can’t be changed at all, you don’t need to worry about whether such changes will be visible. The only ways to allow the method and the anonymous inner class see each other’s changes is to use a mutable type of some description. This could be the enclosing class itself, an array, a mutable wrapper type… anything like that. Basically it’s a bit like communicating between one method and another: changes made to the parameters of one method aren’t seen by its caller, but changes made to the objects referred to by the parameters are seen.

总结

Closure 是 Functional Programming(函数式编程)中一个非常重要的概念,理解了闭包的概念也就知道 lambda 以及 nested function 的用法了。

更新时间:

留下评论