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 的闭包有相似之处。它们都允许在一个函数(或方法)内部访问外部作用域的变量,并且即使外部作用域已经执行完毕,这些变量的值依然可以被保留和使用。