一个谜团

如果你用过类似guava这种“伪函数式编程”风格的library的话,那下面这种风格的代码对你来说应该不陌生:

1
2
3
4
5
6
7
8
9
public void tryUsingGuava() {    final int expectedLength = 4;
    Iterables.filter(Lists.newArrayList("123", "1234"), new Predicate<String>() {        @Override
        public boolean apply(String str) {            return str.length() == expectedLength;
        }
    });
}

这段代码对一个字符串的list进行过滤,从中找出长度为4的字符串。看起来很是平常,没什么特别的。

但是,声明expectedLength时用的那个final看起来有点扎眼,把它去掉试试:

error: local variable expectedLength is accessed from within inner class; needs to be declared final

结果Java编译器给出了如上的错误,看起来匿名内部类只能够访问final的局部变量。但是,为什么呢?其他的语言也有类似的规定吗?

在开始用其他语言做实验之前我们先把问题简化一下,不要再带着guava了,我们去除掉噪音,把问题归结为:

为什么Java中的匿名内部类只可以访问final的局部变量呢?其他语言中的匿名函数也有类似的限制吗?

Scala中有类似的规定吗?

1
2
3
4
5
6
7
8
9
10
11
12
  def tryAccessingLocalVariable {    var number = 123
    println(number)
    var lambda = () => {      number = 456
      println(number)
    }
    lambda.apply()
    println(number)
  }

上面的Scala代码是合法的,number变量是声明为var的,不是val(类似于Java中的final)。而且在匿名函数中可以修改number的值。

看来Scala中没有类似的规定。

C#中有类似的规定吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
public void tryUsingLambda ()
{  int number = 123;
  Console.WriteLine (number);
  Action action = () => {      number = 456;
      Console.WriteLine (number);
  };
  action ();
  Console.WriteLine (number);
}

这段C#代码也是合法的,number这个局部变量在lambda表达式内外都可以访问和赋值。

看来C#中也没有类似的规定。

分析谜团

三门语言中只有Java有这种限制,那我们分析一下吧。先来看一下Java中的匿名内部类是如何实现的:

先定义一个接口:

1
2
3
public interface MyInterface {    void doSomething();
}

然后创建这个接口的匿名子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TryUsingAnonymousClass {    public void useMyInterface() {        final Integer number = 123;
        System.out.println(number);
        MyInterface myInterface = new MyInterface() {            @Override
            public void doSomething() {                System.out.println(number);
            }
        };
        myInterface.doSomething();
        System.out.println(number);
    }
}

这个匿名子类会被编译成一个单独的类,反编译的结果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TryUsingAnonymousClass$1
        implements MyInterface {    private final TryUsingAnonymousClass this$0;
    private final Integer paramInteger;
    TryUsingAnonymousClass$1(TryUsingAnonymousClass this$0, Integer paramInteger) {        this.this$0 = this$0;
        this.paramInteger = paramInteger;
    }
    public void doSomething() {        System.out.println(this.paramInteger);
    }
}

可以看到名为number的局部变量是作为构造方法的参数传入匿名内部类的(以上代码经过了手动修改,真实的反编译结果中有一些不可读的命名)。

如果Java允许匿名内部类访问非final的局部变量的话,那我们就可以在TryUsingAnonymousClass$1中修改paramInteger,但是这不会对number的值有影响,因为它们是不同的reference。

这就会造成数据不同步的问题。

所以,谜团解开了:Java为了避免数据不同步的问题,做出了匿名内部类只可以访问final的局部变量的限制。

但是,新的谜团又出现了:

Scala和C#为什么没有类似的限制呢?它们是如何处理数据同步问题的呢?

上面出现过的那段Scala代码中的lambda表达式会编译成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class TryUsingAnonymousClassInScala$$anonfun$1 extends AbstractFunction0.mcV.sp
        implements Serializable {    public static final long serialVersionUID = 0L;
    private final IntRef number$2;
    public final void apply() {        apply$mcV$sp();
    }
    public void apply$mcV$sp() {        this.number$2.elem = 456;
        Predef..MODULE$.println(BoxesRunTime.boxToInteger(this.number$2.elem));
    }
    public TryUsingAnonymousClassInScala$$anonfun$1(TryUsingAnonymousClassInScala $outer, IntRef number$2) {        this.number$2 = number$2;
    }
}

可以看到number也是通过构造方法的参数传入的,但是与Java的不同是这里的number不是直接传入的,是被IntRef包装了一层然后才传入的。对number的值修改也是通过包装类进行的:this.number$2.elem = 456;

这样就保证了lambda表达式内外访问到的是同一个对象。

再来看看C#的处理方式,反编译一下,发现C#编译器生成了如下的一个类:

1
2
3
4
5
6
7
8
9
10
private sealed class <tryUsingLambda>c__AnonStorey0
{  internal int number;
  internal void <>m__0 ()
  {      this.number = 456;
      Console.WriteLine (this.number);
  }
}

把number包装在这个类内,这样就保证了lambda表达式内外使用的都是同一个number,即便重新赋值也可以保证内外部的数据是同步的。

小结

Scala和C#的编译器通过把局部变量包装在另一个对象中,来实现lambda表达式内外的数据同步。

而Java的编译器由于未知的原因(怀疑是为了图省事儿?)没有做包装局部变量这件事儿,于是就只好强制用户把局部变量声明为final才能在匿名内部类中使用来避免数据不同步的问题。

转载于:https://www.cnblogs.com/snake-hand/p/3151172.html

为什么必须是final的呢?相关推荐

  1. public static final int REMIN_REQUEST_CODE = 0x911 自己的大致理解

    public static final int REMIN_REQUEST_CODE = 0x911; 自己理解为 一个静态常量,也就一个标识,自己目前主要在2个地方常用到 OnActivityRes ...

  2. java内部类的权限符,static介绍、内部类、final、权限修饰符的作用范围,

    static介绍.内部类.final.权限修饰符的作用范围,static 关键字:(可用于修饰方法.变量) static 特点: static是静态修饰符,一般修饰成员变量.被static修饰的属于全 ...

  3. java增加final,Java8增加功能--Effectively final 功能

    java8新增了很多功能,可以大大简化代码,这个系列将会一一辅助代码加以介绍. 局部内部类和匿名内部类访问的局部变量必须由final修饰,java8开始,可以不加final修饰符,由系统默认添加.ja ...

  4. java语言基础final_java语言中final的用法

    许多程序设计语言都有自己的办法告诉编译器某个数据是"常数".常数主要应用于下述两个方面: (1) 编译期常数,它永远不会改变 (2) 在运行期初始化的一个值,我们不希望它发生变化 ...

  5. C++11之final关键字

    一.禁用继承 C++11中允许将类标记为final,方法时直接在类名称后面使用关键字final,如此,意味着继承该类会导致编译错误. 实例如下: class Super final {//...... ...

  6. (1)访问控制 (2)final关键字 (3)对象创建的过程 (4)多态

    1.访问控制(笔试题) 1.1 常用的访问控制符 public - 公有的 protected - 保护的 啥也不写 - 默认的 private - 私有的 1.2 访问控制符的比较 访问控制符 访问 ...

  7. 深入java_深入Java Final

    JAVA关键字final用于修饰数据.方法或类,通常意味着"无法改变的",既数据不能改变,方法不能覆盖,类不能继承.一般采用final有两种原因:设计和效率.而随着JAVA版本的更 ...

  8. Java学习总结:11(final关键字)

    final关键字 在Java中final称为终结器,在Java中可以使用final定义类.方法和属性. 一.使用final定义的类不能再有子类,即:任何类都不能继承以final声明的父类. 在设计类的 ...

  9. java this final_Java this、final等关键字总结

    this 关键字this引用对象自身.它也可以在构造方法内部用于调用同一个类的其他构造方法. 隐藏的静态变量可以通过"类.静态变量"来引用,而隐藏的实例变量就需要使用"t ...

最新文章

  1. prim算法_最小生成树的本质是什么?Prim算法道破天机
  2. feign 第一次调用超时_Feign ,3步搞定 HTTP 请求
  3. 2019年,你需要关注这些Node API和Web框架
  4. JavaScript的键盘事件
  5. java 8 stream reduce详解和误区
  6. 如何远程调试部署在CloudFoundry平台上的nodejs应用
  7. php pdo fetchassoc,pdo执行fetch查询语句,出现500错误,请问应该怎么写
  8. memcached并发CAS模式
  9. pandas小记:pandas汇总统计函数
  10. Spring Cloud(二):服务注册与发现 Eureka【Finchley 版】
  11. 怎么把java文件编译为class文件
  12. 如何在VMware Workstation上安装Windows Home Server Beta“ Vail”
  13. bum报文_数据中心VxLAN技术概念和原理解读
  14. 各类邮箱谷歌邮箱、Outlook邮箱、雅虎邮箱的购买养号策略
  15. 新零售O2O商城系统要怎么开发,这些功能都要有
  16. 新物种爆炸:认知升级时代的新商业思维
  17. 用JAVA怎么做个视图_java问题 可以把用JFrame做出来的具有视图层的小程序放在桌面上当一个小软件吗?...
  18. Lamp 架构 搭建 论坛网站
  19. linux反复出现文件系统损坏,Linux日常维护之文件系统损坏后的修复
  20. HTB打靶(Active Directory 101 Mantis)

热门文章

  1. linux的网络地址配置,教你如何完成Linux网络地址配置
  2. oracle主从关系表查询,Oracle 主从表联合查询解决方法
  3. 广联达2018模板算量步骤_老师傅带你学造价,广联达GTJ2018图文详解,小白也能学会的软件...
  4. python实战演练_python实战演练(三)购物车程序
  5. mysql锁总结知乎_Mysql悲观锁乐观锁区别与使用场景
  6. win7计算机创建新用户,win7系统无法创建新用户的解决方法
  7. python 英文字符频率统计 采用降序方式输出_Python读取英文文件并记录每个单词出现次数后降序输出示例...
  8. Python,OpenCV轮廓属性、轮廓检测及绘制
  9. 5. 编程规范和编程安全指南--JavaScript
  10. keras 的 example 文件 babi_memnn.py 解析