lambda表达式或者匿名函数中为什么要求外部变量为final

x33g5p2x  于2021-12-26 转载在 其他  
字(2.9k)|赞(0)|评价(0)|浏览(570)

1、参考博客

  1. 关于Lambda表达式里面修改外部变量问题
  2. JDK8之前,匿名内部类访问的局部变量为什么必须要用final修饰

2、匿名内部类

在jdk7之前,匿名内部类访问外部类的局部变量时,那么这个局部变量必须用final修饰符修饰,如下图1所示。jdk8则不需要,但是我们在使用这个局部变量时,无法改变局部变量的值,否则编译会报错。

这个特点为什么要求是final类的呢?
因为在java设计之初为了保护数据的一致性而规定的。对引用变量来说是引用地址的一致性,对基本类型来说就是值的一致性。
注意:在JDK8之后,匿名内部类引用外部变量时虽然不用显式的用final修饰,但是这个外部变量必须和final一样,不能被修改(这是一个坑)。解决方案:可以通过定义一个相同类型的变量b,然后将该外部变量赋值给b,匿名内部类引用b就行了,然后就可以继续修改外部变量。

3、lambda表达式

这是因为:Java会将result的值作为参数传递给Lambda表达式,为Lambda表达式建立一个副本,它的代码访问的是这个副本,而不是外部声明result变量。可能很多同学会问为什么非要建立副本呢,直接访问外部的result变量得多方便呢。答案是:这是不可能滴,因为result定义在栈中,当Lambda表达式被执行的时候,result可能已经被释放掉了。

当然啦,你要是一定要在Lambda表达式里面修改外部变量的值也是可以的,可以将变量
定义为实例变量或者将变量定义为数组。

① 情形一

如下图所示:在使用外部类的局部变量时,如果试图修改值,就会编译报错。

② 情形二

如果局部变量是对象类型,则对象的引用地址不可改变,如下图所示。但是如果在局部变量中修改对象是没有问题的(第二篇博客有详细解释)。

③ 情形三

在项目中,我遇到过这种情况:有两个集合,我先对一个集合进行遍历,在这里面又需要对另外一个集合遍历。当时出现了编译报错:variable used in lambda expression should be final or effectively final(lambda表达式中使用的变量应该是final或effective final),位置在下图的红框内。如下图所示。但是下图当我工作之余究其缘由调试时的demo,此时编译又不报错了。我能力有限,暂时还不清楚问题出现的原因。当时我的解决方案有两个:一个是在将lambda表达式换成了for下标遍历,就OK了。另一种方案是创建一个报错的同类型新对象,将list1的引用赋值给新对象,编译也不报错了(如下下图所示)。

为什么lambda表达式使用的局部变量要是final的

为什么 Lambda 表达式(匿名类) 不能访问非 final 类型的局部变量呢?

之前我一直认为是由于Lambda 表达式(匿名类) 会在另一个线程中执行,实例变量存在堆中,而局部变量是在栈上分配,如果在线程中要直接访问一个局部变量,可能线程执行时该局部变量已经被销毁了。

但是有人提出这个原因存在问题,说他们是在同一个线程中运行的,大兄弟提供的验证代码如下:

public class Java8Tester {

    public static void main(String[] args) {
        String str = "Hello, ";
        Person person = name -> {
            System.out.println(str + name + " " + Thread.currentThread().getId());
            System.out.println("匿名类" + " " + Thread.currentThread().getId());
        };
        System.out.println("主方法" + " " + Thread.currentThread().getId());
        person.say("JC");
    }

    interface Person {
        void say(String name);
    }
}

运行之后的结果显示确实都是main线程运行的,结果如下:

我又去看了一些其他的博客,有人说是Java为了防止数据不同步而规定的,也就是为了防止在lambda表达式内使用的外层局部变量被外层代码修改之后内部无法同步这个修改。想看原贴可以点我

我觉得上面超链接中讲述的原因是比较好的,如果其他同学有不同意见欢迎一起交流一起进步。

Java 8 的 Lambda 可以捕获什么变量呢?

(1). 捕获实例变量或静态变量是没有限制的 (可认为是通过 final 类型的局部变量 this 来引用前两者);

(2). 捕获的局部变量必须显式的声明为 final 或实际效果的的 final 类型
注意(敲黑板):如果在Lambda表达式中使用局部变量,即使我们没有声明成final类型的,编译器也会帮助我们将他们声明成final的,所以如果重新赋值会出错。

如下:

结果如下:

此时程序不会报错,虽然我们没有使用final修饰变量b,但是Lambda表达式中也能使用,这是因为编译器帮我们自动声明成final的。

众所周知,final类型的变量不能重新赋值,来验证下是不是编译器真的帮我们声明成了final,如下:

报错:

那么是什么时候帮我们声明成final呢?答案是在创建变量的时候直接声明成final的,测试如下:

总结:在Lambda表达式中可以捕获静态变量和实例变量,但是如果想要捕获局部变量的时候就需要声明成final的,即使我们不主动声明,编译器也会为我们自动声明成final的,不能再重新赋值。也就是Lambda表达式中访问的局部变量(隐式被声明为final的)是可读不可写的,但是Lambda表达式中访问的实例变量和静态变量是可读可写的。

为什么会这样?

在Java中lambda表达式是匿名类语法上的进一步简化,它的本质其实还是调用对象的方法。lambda表达式会以内联的形式创建一个函数式接口的实例,保存在堆中,而局部变量则保存在栈中,而在Java中方法调用是值传递的(特别声明java中都是按值传递的!!!),所以在lambda表达式中对变量的操作都是基于原变量的副本,不会影响到原变量的值。那假如没有要求lambda表达式外部变量为final修饰,那么就会误以为外部变量的值能够在lambda表达式中被改变,而这实际是不可能的,所以要求外部变量为final。
我们知道局部变量随着方法的调用会被压入栈中,当方法调用结束时,出栈,这些局部变量全部死亡。而函数式接口实例对象生命周期和其他类对象是一样的,从创建一个实例对象开始,系统就会为该对象分配内存,直到没有引用变量指向分配给该对象得内存,它被GC垃圾回收,所以很可能出现的一种情况就是:方法已调用结束,局部变量已死亡,但实例对象的对象仍然活着。也就是说如果这个时候仍然有引诱变量指向lambda表达式在堆中所创建的实例,但是lambda表达式中的局部变量已经死亡,岂不是出了问题,这在java中是不允许的,在我还需要你的时候你的局部变量是不可以死亡的。

以上就是我对这个点的理解,小伙伴们有什么不同的见解或者文章中有什么错误的地方,欢迎大家在里评论区留言

相关文章