一个谜团

如果你用过类似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才能在匿名内部类中使用来避免数据不同步的问题。

为什么必须是final的呢?的更多相关文章

  1. java抽象、接口 和final

    抽象 一.抽象类:不知道是具体什么东西的类. abstract class 类名 1.抽象类不能直接new出来. 2.抽象类可以没有抽象方法. public abstract class USB { ...

  2. Java内部类final语义实现

    本文描述在java内部类中,经常会引用外部类的变量信息.但是这些变量信息是如何传递给内部类的,在表面上并没有相应的线索.本文从字节码层描述在内部类中是如何实现这些语义的. 本地临时变量 基本类型 fi ...

  3. Java关键字final、static

    一.final根据程序上下文环境,Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类.非抽象类成员方法和变量.你可能出于两种理解而需要阻止改变:设计或效率. final ...

  4. JavaSE 之 final 初探

    我们先看一道面试题: 请问 final 的含义是什么?可以用在哪里?其初始化的方式有哪些? 首先我们回答一下这道题,然后再探究其所以然.  1.final 表示“最终的”.“不可改变的”,意指其修饰类 ...

  5. PHP的final关键字、static关键字、const关键字

    在PHP5中新增加了final关键字,它可以加载类或类中方法前.但不能使用final标识成员属性,虽然final有常量的意思,但在php中定义常量是使用define()函数来完成的. final关键字 ...

  6. final修饰符

    final本身的含义是"最终的,不可变的",它可以修饰非抽象类,非抽象方法和变量.注意:构造方法不能使用final修饰,因为构造方法不能被继承,肯定是最终的. final修饰的类: ...

  7. 浅析Java中的final关键字(转载)

    自http://www.cnblogs.com/dolphin0520/p/3736238.html转载 一.final关键字的基本用法 在Java中,final关键字可以用来修饰类.方法和变量(包括 ...

  8. java关键字extends(继承)、Supe(父类引用空间)、 This(方法调用者对象)、Instanceof(实例类型-判断对象是否属于某个类)、final(最终)、abstract(抽象) 、interface(接口)0

    java 继承使用关键字extends   继承的作用:减少代码量,优化代码 继承的使用注意点: 1子类不能继承父类的私有变量 2.子类不能继承父类的构造方法 3.子类在调用自己的构造方法时 会默认调 ...

  9. files list file for package 'xxx' is missing final newline

    #!/usr/bin/python # 8th November, 2009 # update manager failed, giving me the error: # 'files list f ...

  10. final关键字(final是最终的)

    final关键字(final是最终的) 1.final修饰特点 a.修饰类,类不能被继承 b.修饰变量,变量就变成了常量, 修饰基本数据类:final int num = 10; 修饰引用数据类型变量 ...

随机推荐

  1. Send Mail 网址

    http://www.codeproject.com/Tips/371417/Send-Mail-Contact-Form-using-ASP-NET-and-Csharp http://www.c- ...

  2. iOS 开发 Message Digest Algorithm 5(MD5加密)

    MD5的全称是Message Digest Algorithm 5(消息摘要算法第五版),是计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护.在90年代初由MIT Laboratory ...

  3. 第一篇:GCD的使用

    一.主队列介绍 主队列是和主线程相关的队列,主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行. 提示:如果把任务放到主队列进行处理,那么不论处理函数是异步的还是同步的都 ...

  4. uboot: 理解uboot要看哪些书

    概览:

  5. 版本管理工具:linux下svn的基本使用

    参考: linux下安装SVN    http://jingyan.baidu.com/article/3c343ff7039de20d37796306.html svn客户端使用linux篇  ht ...

  6. 字符设备驱动: register_chrdev和register_chrdev_region

    概述: register_chrdev与unregister_chrdev配对使用: /*register_chrdev = __register_chrdev_region (一次性256个子设备, ...

  7. Java 重入锁 ReentrantLock

    本篇博客是转过来的. 但是略有改动感谢 http://my.oschina.net/noahxiao/blog/101558 摘要 从使用场景的角度出发来介绍对ReentrantLock的使用,相对来 ...

  8. 设置QPushButton的平面与突出(遍历控件)

    #include "ui_maindialog.h" #include "maindialog.h" #include <QState> #incl ...

  9. ACM 做题过程中的一些小技巧。

    ACM做题过程中的一些小技巧. 1.一般用C语言节约空间,要用C++库函数或STL时才用C++; cout.cin和printf.scanf最好不要混用. 2.有时候int型不够用,可以用long l ...

  10. Windows VS下搭建cocos2d-x环境搭建

    VS2010以上版本(eg:VS2012/VS2013,这里本人用VS2013) 1.环境.安装包准备 2.python安装 3.cocos2d-x安装包解压安装 4.环境变量配置 5.执行setup ...