zoukankan      html  css  js  c++  java
  • 为什么Java中lambda表达式不能改变外部变量的值,也不能定义自己的同名的本地变量呢?

    作者:blindpirate
    链接:https://www.zhihu.com/question/361639494/answer/948286842
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    TL;DR的回答如下:

    JLS 15.27.2 提到:

    The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.

    在Java的线程模型中,栈帧中的局部变量是线程私有的,永远不需要进行同步。假如说允许通过匿名内部类把栈帧中的变量地址泄漏出去(逃逸),就会引发非常可怕的后果:一份“本来被Java线程模型规定永远是线程私有的数据”可能被并发访问!哪怕它不被并发访问,栈中变量的内存地址泄漏到栈帧之外这件事本身已经足够危险了,这是Java这种内存安全的语言绝对无法容忍的(来自评论区

    补充)。

    这才是本质原因。


    下面是比较长的答案。对于如下代码:

    public void doSomething() {
        int value = 0;
        IntStream.range(0, 10).forEach(i -> value++ );
    }

    你会得到一个编译错误:Variable used in lambda expression should be final or effectively final。如果从头分析一下跟这个问题相关的知识,事情要从Java 8之前就存在的匿名内部类说起:

    public void doSomething() {
        int value = 0;
        Executors.newSingleThreadExecutor().submit(new Runnable() {
            @Override
    	public void run() {
    	    value++;
    	}
        });
    }

    同样,你会得到一个编译错误:Variable is accessed within inner class. Needs to be declared final.

    第一个问题,为什么存在这样的限制?

    要回答这个问题,我们需要首先明白,匿名内部类外面的value和里面的value是同一个内存地址中的数据么?

    很明显不是,因为我们都知道,局部变量存在于栈帧的局部变量表中,一旦方法结束,栈帧被销毁,这个变量(这份数据)就不再存在,但是匿名内部类中的value可能在栈帧销毁后继续存在(比如在这个例子中,匿名内部类被提交到了线程池中)。

    所以,只有一个可能,在匿名内部类被创建的时候,被捕获的局部变量发生了复制。如果我们允许在匿名内部类中执行value++操作,带来的后果就是,匿名内部类中的value的拷贝被更新了,但是原先的value不会受到任何影响(因为它可能已经不存在了)——你看上去好像两个value是同一个地址,同一份数据,但是实际上发生了拷贝,和方法调用的值传递如出一辙。这是很可怕的一件事情,它会让你误以为,在匿名内部类中执行value++会改变原先的局部变量value。

    这还不是最可怕的。最可怕的是,如果允许匿名内部类修改外面的局部变量,会颠覆掉整个Java线程模型!!!!!!

    JLS 15.27.2 提到:

    The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems.

    在Java的线程模型中,栈帧中的局部变量是线程私有的,永远不需要进行同步。但是,假如说我们通过匿名内部类把栈帧中的变量地址泄漏出去,就会引发非常可怕的后果:一份“本来被Java线程模型规定永远是线程私有的数据”可能被并发访问!!!

    因此,在Java 8之前,编译器会强迫你加上一个final关键字:

    public void doSomething() {
        final int value = 0;  // 不声明final不给过编译,你给老子死了这条修改的心吧
        Executors.newSingleThreadExecutor().submit(new Runnable() {
            @Override
    	public void run() {
    	    System.out.println(value);
    	}
        });
    }

    第二个问题:那为什么Java 8之后我可以不写final了呢?

    Java 8引入了lambda表达式,我们从此可以非常方便地编写大量的小代码块,但是在捕获外围的局部变量这件事上,lambda表达式和匿名内部类没有任何区别——被捕获的局部变量必须是final的。这就带来了一个问题,继续坚持把局部变量声明成final的话,烦也烦死了。 因此,JLS做出了一个妥协:

    假如一个局部变量在整个生命周期中都没有被改变(指向),那么它就是effectively final的——换句话说,不是final,胜似final。这样的局部变量也允许被lambda表达式或者匿名内部类所捕获,不过只能看不能摸——可以读取,但是不能修改。

    下一个问题是,老子就是想在lambda表达式里面改外面的值!你咬我啊!

    IDEA早已看穿了一切:

    还记得我在之前的文章中强调的么?任何错误,你都可以按万能键Alt+Enter:

    blindpirate:「每日一题」走上人生巅峰的快捷键​zhuanlan.zhihu.com图标

    为什么转换成一个AtomicInteger就可以了呢?这跟线程安全没有半毛钱关系,纯粹是利用了这样一个技巧:AtomicInteger可以当作int的容器。因为它是在堆上被分配的,我们完全没有改变这个局部变量的指向(effectively final成立),就达到了修改其中数据的目的。

  • 相关阅读:
    java~用域名回显照片
    java~-照片--用流回显源码
    java表单+多文件上传~~源代码
    java~生成二维码源代码
    html页面悬浮框--左边动画(隐藏凸出)---css设置
    html页面悬浮框--右边动画(隐藏凸出)---css设置
    java编写二维码
    java上传---表单+多文件上传
    js--a标签带参数href取值
    爬虫杂记
  • 原文地址:https://www.cnblogs.com/xiaoxiaoyu0707/p/14230981.html
Copyright © 2011-2022 走看看