作者: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中lambda表达式不能改变外部变量的值,也不能定义自己的同名的本地变量呢?的更多相关文章

  1. Java中lambda表达式详解

    原文地址:http://blog.laofu.online/2018/04/20/java-lambda/ 为什么使用lambda 在java中我们很容易将一个变量赋值,比如int a =0;int ...

  2. Java中Lambda表达式的进化之路

    Lambda表达式的进化之路 为什么要使用Lambda表达式 可以简洁代码,提高代码的可读性 可以避免匿名内部类定义过多导致逻辑紊乱 在原先实现接口抽象方法的时候,需要通过定义一个实现接口的外部类来实 ...

  3. 快速掌握Java中Lambda表达式的用法

    Lambda表达式的作用: Lambda表达式的作用就是简化代码开发,让代码看起来更加简介.它是用来简化匿名内部类的.但是并不是所有的匿名内部类都能用Lambda表达式简化,Lambda表达式是有使用 ...

  4. Java中Lambda表达式的使用

    简介(译者注:虽然看着很先进,其实Lambda表达式的本质只是一个"语法糖",由编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能.本人建议不要乱用,因 ...

  5. Java中Lambda表达式的使用(转)

    https://www.cnblogs.com/franson-2016/p/5593080.html 简介(译者注:虽然看着很先进,其实Lambda表达式的本质只是一个"语法糖" ...

  6. Java中Lambda表达式的简单使用

    Lambda表达式是Java SE 8中一个重要的新特性.你可以把 Lambda表达式 理解为是一段可以传递的代码 (将代码像数据一样进行传递).可以写出更简洁.更灵活的代码.作为一种更紧凑的代码风格 ...

  7. Java中lambda表达式学习

    一.Lambda表达式的基础语法: Java8中引入了一个新的操作符"->"该操作符称为箭头操作符或Lambda操作符,箭头操作符将Lambda表达式拆分为两部分: 左侧:L ...

  8. Java中lambda(λ)表达式的语法

    举一个排序的例子,我们传入代码来检查一个字符串是否比另一个字符串短.这里要计算: first.length() - second.length() first和second是什么?他们都是字符串.Ja ...

  9. Java中Lambda表达式基础及使用详解

    概述 Lambda 是JDK 8 的重要新特性.它允许把函数作为一个方法的参数(函数作为参数传递进方法中),使用 Lambda 表达式可以使代码变的更加简洁紧凑,使Java代码更加优雅. 标准格式 三 ...

随机推荐

  1. 第15.46节、PyQt显示部件:OpenGL Widget部件功能简介及使用案例

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 一.概述 OpenGL Widget部件是一个Op ...

  2. PyQt(Python+Qt)学习随笔:QListWidget获取指定位置对应项的itemAt方法

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 itemAt方法根据参数给定位置返回列表部件中的项.对应语法如下: QListWidgetItem ...

  3. PyQt(Python+Qt)学习随笔:Model/View架构中的Model模型概念

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 Model/View架构中的Model模型Model与数据源通信,为体系结构中的其他组件提供数据接口 ...

  4. STL——容器(Set & multiset)的大小

    1. set.size();  //返回容器中元素的数目 2. set.empty();//判断容器是否为空 empty() 是由一下代码实现的,可以发现,当长度为0时返回 false,以此判断容器为 ...

  5. JSON类型解析

    JSON类型解析 一.序列化和反序列化 程序中的对象,如python中的字典.列表.函数.类等,都是存在内存中,不方便传递或存储,所以需要将内存中的对象转化为文本或者文件格式,来满足传输和持久化(存储 ...

  6. 如何使用交易开拓者(TB)开发数字货币策略

    更多精彩内容,欢迎关注公众号:数量技术宅.想要获取本期分享的完整策略代码,请加技术宅微信:sljsz01 为何使用交易开拓者(TB)作为回测工具 交易开拓者(后文以TB简称)是一个支持国内期货市场K线 ...

  7. Linux下设置定时任务删除归档日志

    1.编辑删除归档日志的脚本----/home/oracle/clearlog.sh #! /bin/bash source ~/profile_orcl#记录归档删除的日志exec >> ...

  8. 淘宝|蚂蚁|菜鸟|盒马|嘀嘀|饿了么面经(已拿多个offer)

    上一篇的同学拿到了bigo和腾讯的offer,这一次的分享来自两位同学的面试综合,他们分别拿到了菜鸟.嘀嘀.盒马的多个offer,由于面试的时间跨度时间太长,且面试的部门太多,只能回忆到具体的面试题, ...

  9. HBase按照行键范围删除数据

    #!/bin/bash #TOOL_PATH=$(cd "$(dirname "$0")"; pwd) #TOOL_PATH_TMP=$(cd "$( ...

  10. 面试 16-01.MVVM

    16-01.MVVM #前言 MVVM的常见问题: 如何理解MVVM 如何实现MVVM 是否解读过Vue的源码 题目: 说一下使用 jQuery 和使用框架的区别 说一下对 MVVM 的理解 vue ...