lambda访问外部变量
前言
使用lambda开发时,在表达式内部获取需要捕获外部变量,同时进行修改,这时idea编译器可能就提示你:Variable used in lambda expression should be final or effectively final.
分析
通过几个例子来分析下:
示例1:lambda表达式内部修改外部变量Integer,编译失败
@Test
public void test1 () {
List<Long> list = Arrays.asList(1L, 2L);
Integer i = 256;
// 对象唯一标识符
System.out.println(System.identityHashCode(i));
list.forEach((item) -> {
// 编译失败
// i++;
// 对象唯一标识符,与上面的相对
System.out.println(System.identityHashCode(i));
System.out.println(item + ":" + i);
});
}
示例2:将Interger修改为AtomicInteger,内部修改,编译运行正常
@Test
public void test2 () {
List<Long> list = Arrays.asList(1L, 2L);
AtomicInteger i = new AtomicInteger();
list.forEach((item) -> {
i.getAndIncrement();
System.out.println(item + ":" + i);
});
}
示例3:将Interger修改为只有1个元素的数组,内部修改,编译运行正常
@Test
public void test3 () {
List<Long> list = Arrays.asList(1L, 2L);
final Integer[] i = {0};
list.forEach((item) -> {
i[0] = i[0] + 1;
System.out.println(item + ":" + i[0]);
});
}
示例4:将Interger修改为外部变量Integer,编译运行正常
private Integer num = 256;
@Test
public void test4 () {
List<Long> list = Arrays.asList(1L, 2L);
list.forEach((item) -> {
num++;
System.out.println(item + ":" + num);
});
}
首先需要理解下:
1、在Java的线程模型中,栈帧中的局部变量是线程私有的,不需要考虑线程安全。
----虚拟机栈结构:
------当前线程:{当前栈帧[局部变量表、操作数栈、动态链接、返回地址等]}{栈帧[…]}{栈帧[…]}…
------线程N:{当前栈帧[局部变量表、操作数栈、动态链接、返回地址等]}{栈帧[…]}{栈帧[…]}…
2、java 是值传递,如果是个对象,传递的是引用地址,可以通过 System.identityHashCode(i);看对象唯一标识符(见示例1)
所以:
线程模型中,每个线程有多个栈帧,每个栈帧有自己的局部变量表,可以理解成示例1中Integer变量,该变量是线程内绝对安全的,在经过lambda表示式后,后续的代码再访问不应该有线程安全问题,不然就违背了线程模型,那么经过lambda表示式传递给匿名内部类后,它是将地址传递了出去,一旦地址值在另一个线程中改变,这时就会导致程序结果混乱,出现线程安全问题,故而直接简单点不让修改引用值。
结论
1、线程局部变量泄漏出去,无法保障安全,所以需要final,不能修改"值"(见示例1)
2、AtomicInteger/数组 可以当作int的容器。因为它是在堆上被分配的,我们完全没有改变这个局部变量的指向(effectively final成立)(见示例2,示例3)
3、非线程局部变量传递给lambda(匿名内部类),属线程共享变量,需要自行保证线程安全(见示例4)
总结
lambda(匿名内部类)允许访问线程域内外部变量,如果还允许修改,会导致线程内的私有变量存在不安全的行为,颠覆java线程模型,所以java语言规范给限制了。