CodeGym /课程 /JAVA 25 SELF /Java 中的闭包(closure):要点

Java 中的闭包(closure):要点

JAVA 25 SELF
第 48 级 , 课程 3
可用

1. 初识闭包

闭包 是一种函数(或函数对象),它不仅使用自己的参数,还会“记住”创建它时所在环境中的变量。简单说,如果方法内部的 lambda 表达式或匿名类使用了该方法的局部变量,它就成为一个闭包。

通俗示例

public class ClosureDemo {
    public static void main(String[] args) {
        String greeting = "你好,";
        Runnable sayHello = () -> System.out.println(greeting + "世界!");
        sayHello.run(); // 输出:你好,世界!
    }
}

这里,lambda “捕获”了外部方法中的变量 greeting 并在自身内部使用。这就是闭包!

2. 有效 final 变量:是什么,为什么需要?

在 Java 中,lambda 表达式(以及匿名类)只能使用来自外部方法、被声明为 final 的变量,或那些在初始化后未被修改的变量。这样的变量称为 有效 final

为什么?

这是因为 lambda 表达式可能会在创建它的方法结束执行之后才被调用。如果变量还能继续改变,就会产生歧义:到底该用变量的哪个版本?为避免这种“惊喜”,Java 要求变量必须不可变(或者至少看起来不可变)。

示例:正确的用法

public static void main(String[] args) {
    int number = 42; // number 是有效的 final
    Runnable r = () -> System.out.println(number);
    r.run(); // 42
}

示例:尝试在使用后修改变量

public static void main(String[] args) {
    int number = 42;
    Runnable r = () -> System.out.println(number);
    number++; // 错误:变量 number 必须是 final 或有效 final
    r.run();
}

编译器会报错:Variable used in lambda expression should be final or effectively final

所谓“有效 final”是……变量只被赋值一次,之后不再改变。并不一定要显式写 final,编译器会自行判断。

3. lambda 表达式如何捕获变量?

当你编写使用外部变量的 lambda 时,Java 会把该变量与 lambda 一起“打包”。即使创建该 lambda 的方法已经结束,变量也不会消失——它在闭包内部仍然存在。

示意:lambda “记住”变量

public static Runnable createGreeter(String name) {
    // name — 参数,会被 lambda 捕获
    return () -> System.out.println("你好," + name + "!");
}

public static void main(String[] args) {
    Runnable greeter = createGreeter("Vasya");
    greeter.run(); // 你好,Vasya!
}

这里变量 name 已经不在 main 方法的栈中,但 greeter 仍然“记得”它的值。

内部是如何实现的?

Java 编译器会创建一个专用的辅助对象(也称为“capture/display class”),用于保存所有被捕获的变量。lambda 表达式会变成一个对象,并持有指向这个“容器”的引用。

4. 闭包示例:返回一个使用外部变量的函数

我们来写一个函数,返回一个使用其上下文变量的 lambda:

import java.util.function.IntSupplier;

public class ClosureFactory {
    public static IntSupplier makeAdder(int x) {
        // x — 被 lambda 捕获
        return () -> x + 10;
    }

    public static void main(String[] args) {
        IntSupplier adder = makeAdder(5);
        System.out.println(adder.getAsInt()); // 15
    }
}

这里变量 x 已经离开了方法栈,但 lambda 仍可以使用它。

5. 为什么不能修改被捕获的变量?

public static void main(String[] args) {
    int base = 100;
    Runnable printer = () -> System.out.println(base);
    base = 200; // 错误!
    printer.run();
}

编译器不允许这么做。若允许修改 base,将会不清楚 lambda 中该使用变量的旧值还是新值。因此 Java 禁止修改被 lambda 捕获的局部变量。

lambda 中可以使用什么?

  • 初始化后不再改变的局部变量(有效 final)。
  • 类的字段(包括 static 和非静态)——它们可以改变,但这属于访问对象状态,而不是捕获局部变量。

6. 与匿名类的对比

在 Java 中,在出现 lambda 表达式之前,可以用匿名类实现闭包:

public static void main(String[] args) {
    String word = "Java";
    Runnable r = new Runnable() {
        public void run() {
            System.out.println(word);
        }
    };
    r.run(); // Java
}

规则相同:变量 word 必须是 final 或有效 final。

区别:this 的作用域

  • 在匿名类中,this 指向匿名类的实例。
  • 在 lambda 表达式中,this 指向外部对象(例如当前类的实例)。

7. 闭包与类字段

如果 lambda 使用的是类的字段,这并不属于严格意义上的“捕获局部变量”——字段始终可访问,并且可以被修改。

public class Counter {
    private int count = 0;

    public Runnable makeCounter() {
        return () -> {
            count++;
            System.out.println("计数器: " + count);
        };
    }

    public static void main(String[] args) {
        Counter c = new Counter();
        Runnable r = c.makeCounter();
        r.run(); // 计数器: 1
        r.run(); // 计数器: 2
    }
}

8. Java 中闭包的常见错误与注意点

错误 1:试图修改被 lambda 捕获的变量。 最常见的错误是:在变量被用于 lambda 之后仍尝试修改该局部变量。编译器会提示:Variable used in lambda expression should be final or effectively final

错误 2:以为变量会被“冻结”。 在 Java 中,当涉及类字段时,被捕获的并不是值的副本,而是对原始字段的访问。如果类字段发生变化,lambda 会看到新值。但在 lambda 中使用的局部变量必须是有效 final。

错误 3:以为 lambda 会为 this 创建新的作用域。 在 lambda 中,this 指向外部对象(容器类)。在匿名类中,this 指向匿名类自身。

错误 4:使用可变对象。 如果你捕获的是对可变对象(例如 List)的引用,即使变量本身是有效 final,你仍然可以在 lambda 内部修改其内容:

public static void main(String[] args) {
    java.util.List<String> list = new java.util.ArrayList<>();
    Runnable r = () -> list.add("Hello");
    r.run();
    System.out.println(list); // [Hello]
}

这里变量 list 本身没有变化(我们没有做 list = ...),但它内部所指向的对象内容发生了变化。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION