java闭包

闭包是JavaScript和Java中重要的概念,指函数或Lambda捕获外部作用域变量。JavaScript通过内部函数访问局部变量形成闭包;Java中,Lambda或匿名类捕获final变量实现闭包,动态绑定状态,增强函数式编程灵活性。

Posted by Hilda on May 12, 2025

通常讲到闭包,一般都是指在javascript的环境中。闭包是JS中一个非常重要的也非常常用的概念。闭包产生的原因就是变量的作用域范围不同。一般来说函数内部的定义的变量只有函数内部可见。如果想要在函数外部操作这个变量就需要用到闭包了。

JS中的闭包

在JS中,变量可以分为两种全局作用域和局部作用域。在函数外部无法读取函数内部定义的局部变量。

例如:

1
2
3
4
function fun1() {
    var x1 = 10;
}
console.log(x1)

控制台会报错:

image-20250512144802802

在函数fun1中定义了一个局部变量x1,然后尝试从函数外部访问它。结果出错。

虽然函数中定义的变量在函数外部无法被访问。但是在函数中定义的函数中可以访问

1
2
3
4
5
6
7
8
9
function fun1() {
    var x1 = 10;
    function fun2() {
        alert(x1);
    }
    return fun2;
}
var res = fun1();
res();

image-20250512145047587

上面的例子中,在fun1中定义了fun2,在fun2中访问了局部变量x1。最后将fun2返回。接着可以操作返回的函数fun2来对函数中定义的局部变量x1进行操作。

所以得出了闭包的定义:闭包就是定义在函数内部的函数,或者闭包是能够访问函数局部变量的函数

java 中的闭包

在lambda表达式出现之前,java中是没有函数的概念的。和函数差不多相当的就是方法了。

在方法内部可以定义方法的局部变量。我们无法在方法内部定义方法,但是我们可以在方法内部定义匿名类。那么这个匿名类是可以访问方法中定义的局部变量的。如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ClosureTest {
    public Runnable closureExample(){
        int x2 = 90;
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(x2);
            }
        };
        return runnable;
    }

    public static void main(String[] args) {
        ClosureTest closureTest = new ClosureTest();
        Runnable runnable = closureTest.closureExample();
        runnable.run();
    }
}

在上面的方法中,定义了一个局部变量x2。然后创建了一个匿名类runnable。在runnable中,访问了局部变量x2

最后将这个创建的匿名类返回。这样返回的匿名类就包含了对方法局部变量的操作,这样就叫做闭包。

在内部类中,会创建一个新的作用域范围,在这个作用域范围之内,你可以定义新的变量,并且可以用this引用它。

但是在Lambda表达式中,并没有定义新的作用域范围,如果在Lambda表达式中使用this,则指向的是外部类。

注:在 Java 中,内部类是定义在另一个类内部的类。当你在一个内部类中使用 this 时,它指向的是内部类的实例,而不是外部类的实例。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class OuterClass {
    private String name = "Outer";
    public class InnerClass{
        private String name = "Inner";
        public void printName() {
            System.out.println(this.name);
            System.out.println(OuterClass.this.name);
        }
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        innerClass.printName();
    }
}

与内部类不同,Lambda 表达式并没有引入一个新的作用域,它直接继承了外部类的作用域。换句话说,Lambda 表达式内部的 this 关键字不会指向 Lambda 自身,而是指向外部类的实例。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OuterClass {
    private String name = "Outer";
    public void runLambda() {
        Runnable runnable = () -> {
            System.out.println(this.name);
        };
        runnable.run();
    }

    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        outerClass.runLambda();
    }
}

虽然this的指向是不同的,但是在lambda表达式中也是可以访问方法的局部变量:

1
2
3
4
5
public Runnable closureExample(){
    int x2 = 90;
    Runnable runnable = () -> System.out.println(x2);
    return runnable;
}

深入理解lambda表达式和函数的局部变量

为什么 Lambda 表达式没有新的作用域?

Lambda 表达式并不像内部类那样创建一个新的类或作用域。它是在编译时转换为一个匿名类,并且在该匿名类中没有一个单独的 this 指向 Lambda 自身,而是直接使用外部类的 this。因此,Lambda 表达式中的 this 总是指向外部类的实例。

可以通过查看编译后的字节码来确认这一点。Java 编译器会将 Lambda 表达式转换为一个匿名类,这个匿名类实现了目标接口(如 RunnableFunction 等),并将 Lambda 的代码逻辑放在匿名类的 run() 或相应方法中。

验证过程:

1.编写一个简单的 Lambda 表达式示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LambdaTest {
    private String name = "Outer";
    public void test() {
        Runnable runnable = () -> {
            System.out.println(this.name);
        };
        runnable.run();
    }

    public static void main(String[] args) {
        LambdaTest lambdaTest = new LambdaTest();
        lambdaTest.test();
    }
}

2.编译并查看字节码

  • 首先使用 javac LambdaTest.java 编译代码。
  • 然后使用 javap -c LambdaTest 查看字节码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Compiled from "LambdaTest.java"
public class LambdaTest {
  public LambdaTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: ldc           #7                  // String Outer
       7: putfield      #9                  // Field name:Ljava/lang/String;
      10: return

  public void test();
    Code:
       0: aload_0
       1: invokedynamic #15,  0             // InvokeDynamic #0:run:(LLambdaTest;)Ljava/lang/Runnable;
       6: astore_1
       7: aload_1
       8: invokeinterface #19,  1           // InterfaceMethod java/lang/Runnable.run:()V
      13: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #10                 // class LambdaTest
       3: dup
       4: invokespecial #23                 // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #24                 // Method test:()V
      12: return
}

在字节码中,this 被隐式地引用。具体来说,Lambda 表达式是通过 invokedynamic 动态地生成并绑定的。在字节码中的 invokedynamic 指令会链接到相应的 Lambda 实现,而 Lambda 表达式中的 this 会引用外部类(LambdaTest)的实例。

Lambda 表达式的无状态与有状态

Lambda 表达式 是一种简洁的表示方法,它通常用于表示 函数行为。Lambda 表达式的本质是一个匿名的、没有名称的函数。它通常接受一个或多个参数,并且返回一个结果(或者没有返回值)。

无状态 这个概念指的是 Lambda 表达式不持有任何外部或内部的可变状态,它仅仅根据输入来计算输出,不依赖于任何外部的可变数据。

Lambda 表达式的作用通常是接受一定的输入,并根据这些输入计算出一个输出。这个过程是 确定性的,也就是说,对于相同的输入,Lambda 表达式总是会产生相同的输出。

Lambda 表达式不持有可变状态:在传统的面向对象编程中,一个对象可能有 成员变量(状态),这些变量可能在方法调用过程中被修改。而 Lambda 表达式不持有这样的成员变量,它的行为完全由它的输入决定。

但是,如果lambda表达式中引用的方法中的局部变量,则lambda表达式就变成了闭包,因为这个时候lambda表达式是有状态的。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LambdaStatelessTest {
    private Integer x = 1;
    public void returnRunnable() {
        Runnable runnable = () -> x++;
        runnable.run();
        System.out.println(x);
    }

    public static void main(String[] args) {
        LambdaStatelessTest lambdaStatelessTest = new LambdaStatelessTest();
        lambdaStatelessTest.returnRunnable();
    }
}

上面这个例子控制台会输出2

在这个例子中,Lambda 表达式修改了外部变量 x,这就使得它是有状态的。Lambda 表达式访问并修改了 外部的可变状态。我们称这种情况为 闭包(closure),其中 Lambda 表达式捕获了外部变量并能够修改它的值。

这种行为是 有状态 的,因为它依赖于外部状态 x 并且对其进行修改。通常,Lambda 表达式如果在外部状态(如类的字段或局部变量)上进行修改,则被认为是 有状态的

虽然 Lambda 表达式本身是无状态的,但它仍然能够捕获外部状态(如局部变量和字段),并在执行时修改它们。这种情况被称为 闭包,并不是 Lambda 本身的状态,而是 Lambda 捕获了外部的可变状态


为了深入理解lambda表达式和局部变量传值的关系,我们将编译好的class文件进行反编译。

1
2
javac LambdaStatelessTest.java   # 编译
javap -v LambdaStatelessTest.class    # 字节码反编译

image-20250512153045906

getfield #13 指令表示对外部变量 x 的访问,xLambdaStatelessTest 类中的一个实例变量,类型是 Integer

这个字段的访问和修改发生在 Lambda 表达式的 run 方法中,说明 Lambda 表达式是捕获了外部的 可变状态,并且修改了它。