Java 1.8 Lambda 如何访问局部变量,成员变量?

Java 1.8 Lambda 如何访问局部变量,成员变量?

1. 访问局部变量

在 Lambda 表达式中访问局部变量时,该局部变量实际上必须是 final 或者是 "有效 final" 的。"有效 final" 指的是变量在初始化后没有再被重新赋值。

所谓有效 final,就是在一个方法里面,只被定义赋值了一次,相当于是final。

示例代码

ini 复制代码
import java.util.function.Consumer;

public class LambdaLocalVariable {
    public static void main(String[] args) {
        int num = 10; // 有效 final 变量
        Consumer<Integer> consumer = (n) -> System.out.println(n + num);
        // num = 20; // 如果取消注释,编译会报错,因为改变了 num 的值,它不再是有效 final
        consumer.accept(5);
    }
}

原因分析

Lambda 表达式会捕获局部变量的值而不是变量本身。这是因为局部变量存储在栈上,当 Lambda 表达式所在的方法执行完毕后,局部变量可能已经被销毁。为了保证 Lambda 表达式可以正确访问局部变量的值,Java 要求局部变量必须是 final 或者 "有效 final" 的。

Lambda 表达式捕获局部变量的原理

Lambda 表达式会捕获局部变量的值,也就是把局部变量的值复制一份到 Lambda 表达式内部。这样做的原因是,局部变量存储在栈上,其生命周期受限于方法的执行,当方法执行结束后,局部变量可能会被销毁。通过捕获局部变量的值,能保证 Lambda 表达式在后续执行时可以访问到正确的值。

csharp 复制代码
public class LambdaReferenceReassignment {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Initial");

        // 定义一个 Lambda 表达式,捕获局部变量 list
        List<String> finalList = list;
        Consumer<String> consumer = (str) -> {
            try {
                //故意停一会
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            finalList.add(str);
            System.out.println(finalList);
        };

        // 重新赋值引用
        list = new ArrayList<>();

        System.out.println("此刻外面的list=" + list);
        System.out.println("此刻外面的finalList=" + finalList);

        // 调用 Lambda 表达式
        consumer.accept("New ");


    }
}

运行结果:

ini 复制代码
此刻外面的list=[]
此刻外面的finalList=[Initial]
[Initial, New ]

finalList相当于就是被lamda代码给锁住了,保留了当前方法内的快照。

误区:lamda访问的局部变量,一定是不变的。

错了,不是不可变,而是不可重新赋值,确保lamda访问的是同一个变量,你当然可以去操作list,那么lamda中访问的就是最新的list。

2. 访问成员变量

Lambda 表达式访问成员变量时,不需要将成员变量声明为 final,因为成员变量的生命周期与对象的生命周期相同,只要对象存在,成员变量就可以被访问。

示例代码

ini 复制代码
import java.util.function.Consumer;

public class LambdaInstanceVariable {
    private int instanceVar = 10;

    public void testLambda() {
        Consumer<Integer> consumer = (n) -> System.out.println(n + instanceVar);
        instanceVar = 20; // 可以修改成员变量的值
        consumer.accept(5);
    }

    public static void main(String[] args) {
        LambdaInstanceVariable obj = new LambdaInstanceVariable();
        obj.testLambda();
    }
}

原因分析

成员变量存储在堆上,与对象的生命周期相关。Lambda 表达式可以通过对象引用访问成员变量,因此不需要将成员变量声明为 final

3. 访问静态成员变量

Lambda 表达式访问静态成员变量时,也不需要将静态成员变量声明为 final,因为静态成员变量属于类,在类加载时就已经分配了内存,只要类存在,静态成员变量就可以被访问。

示例代码

typescript 复制代码
import java.util.function.Consumer;

public class LambdaStaticVariable {
    private static int staticVar = 10;

    public static void testLambda() {
        Consumer<Integer> consumer = (n) -> System.out.println(n + staticVar);
        staticVar = 20; // 可以修改静态成员变量的值
        consumer.accept(5);
    }

    public static void main(String[] args) {
        testLambda();
    }
}

原因分析

静态成员变量存储在方法区,与类的生命周期相关。Lambda 表达式可以通过类名直接访问静态成员变量,因此不需要将静态成员变量声明为 final

看一个例子:

csharp 复制代码
public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("Initial");

    // 定义一个 Lambda 表达式,捕获局部变量 list
    List<String> finalList = list;
    Runnable runnable = () -> {
        try {
            //故意停一会
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        finalList.add("new");
        System.out.println(finalList);
    };

    new Thread(runnable).start(); //开启线程,但是会过一会再读取到finalList

    //我在这里改了,没想到吧,这会线程还没读到finalList呢
    finalList.add("23456");

}

输出:[Initial, 23456, new]

方法都结束了,线程还在跑,按理说方法结束,finalList在栈上,已经被回收了,但是因为有lamda,就好像js的闭包一样被锁住了,从此,finalList跟着lamda的生命周期混了,延续了下去,很神奇吧。这是一个很有意思的话题了,后期我们专门写一篇文章来聊聊这个事情。

总结

只有在 Lambda 表达式访问局部变量时,才需要局部变量是 final 或者 "有效 final" 的,以确保 Lambda 表达式能够正确访问局部变量的值。而访问成员变量和静态成员变量时,由于它们的生命周期与对象或类相关,不需要声明为 final

Java 的 Lambda 表达式捕获局部变量的机制和 JavaScript 的闭包有相似之处。它们都允许在一个函数(或方法)内部访问外部作用域的变量,并且即使外部作用域已经执行完毕,这些变量的值依然可以被保留和使用。

相关推荐
计算机程序设计开发1 分钟前
器材借用管理系统详细设计基于Spring Boot-SSM
java·spring boot·后端·毕业设计·计算机毕业设计
冷冷清清中的风风火火10 分钟前
在 Spring Boot 结合 MyBatis 的项目中,实现字段脱敏(如手机号、身份证号、银行卡号等敏感信息的部分隐藏)可以通过以下方案实现
spring boot·后端·mybatis
Goboy26 分钟前
老婆问我:“什么是大模型的“上下文”?”
后端·程序员·架构
gnicky28 分钟前
trae和Spring Boot Java 项目 ruoyi框架
java·spring boot·后端·ruoyi·trae
小画家~37 分钟前
第四节:sqlx库使用指南
开发语言·后端·golang
田猿笔记1 小时前
One API:统一接口与模型重定向的强大功能
后端·ai·one api
Imagine Miracle1 小时前
【Rust】集合的使用——Rust语言基础16
开发语言·后端·rust
zyxzyx6663 小时前
Canal 解析与 Spring Boot 整合实战
java·spring boot·后端
Studying_swz4 小时前
Spring WebFlux之流式输出
java·后端·spring
苏墨瀚4 小时前
C#语言的响应式设计
开发语言·后端·golang