轉(zhuǎn)自: http://cuipengfei.me/blog/2013/06/22/why-does-it-have-to-be-final/
一個謎團
如果你用過類似guava這種“偽函數(shù)式編程”風(fēng)格的library的話疏唾,那下面這種風(fēng)格的代碼對你來說應(yīng)該不陌生:
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
結(jié)果Java編譯器給出了如上的錯誤炒事,看起來匿名內(nèi)部類只能夠訪問final的局部變量臀栈。但是,為什么呢挠乳?其他的語言也有類似的規(guī)定嗎权薯?
在開始用其他語言做實驗之前我們先把問題簡化一下,不要再帶著guava了睡扬,我們?nèi)コ粼胍裘蓑迹褑栴}歸結(jié)為:
為什么Java中的匿名內(nèi)部類只可以訪問final的局部變量呢?其他語言中的匿名函數(shù)也有類似的限制嗎卖怜?
Scala中有類似的規(guī)定嗎屎开?
def tryAccessingLocalVariable {
var number = 123
println(number)
var lambda = () => {
number = 456
println(number)
}
lambda.apply()
println(number)
}
上面的Scala代碼是合法的,number變量是聲明為var的马靠,不是val(類似于Java中的final)奄抽。而且在匿名函數(shù)中可以修改number的值。
看來Scala中沒有類似的規(guī)定甩鳄。
C#中有類似的規(guī)定嗎逞度?
public void tryUsingLambda ()
{
int number = 123;
Console.WriteLine (number);
Action action = () => {
number = 456;
Console.WriteLine (number);
};
action ();
Console.WriteLine (number);
}
這段C#代碼也是合法的,number這個局部變量在lambda表達式內(nèi)外都可以訪問和賦值妙啃。
看來C#中也沒有類似的規(guī)定档泽。
分析謎團
三門語言中只有Java有這種限制,那我們分析一下吧揖赴。先來看一下Java中的匿名內(nèi)部類是如何實現(xiàn)的:
先定義一個接口:
public interface MyInterface {
void doSomething();
}
然后創(chuàng)建這個接口的匿名子類:
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);
}
}
這個匿名子類會被編譯成一個單獨的類馆匿,反編譯的結(jié)果是這樣的:
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的局部變量是作為構(gòu)造方法的參數(shù)傳入匿名內(nèi)部類的(以上代碼經(jīng)過了手動修改,真實的反編譯結(jié)果中有一些不可讀的命名)燥滑。
如果Java允許匿名內(nèi)部類訪問非final的局部變量的話渐北,那我們就可以在TryUsingAnonymousClass$1中修改paramInteger,但是這不會對number的值有影響突倍,因為它們是不同的reference。
這就會造成數(shù)據(jù)不同步的問題盆昙。
所以羽历,謎團解開了:Java為了避免數(shù)據(jù)不同步的問題,做出了匿名內(nèi)部類只可以訪問final的局部變量的限制淡喜。
但是秕磷,新的謎團又出現(xiàn)了:
Scala和C#為什么沒有類似的限制呢?它們是如何處理數(shù)據(jù)同步問題的呢炼团?
上面出現(xiàn)過的那段Scala代碼中的lambda表達式會編譯成這樣:
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也是通過構(gòu)造方法的參數(shù)傳入的澎嚣,但是與Java的不同是這里的number不是直接傳入的疏尿,是被IntRef包裝了一層然后才傳入的。對number的值修改也是通過包裝類進行的:this.number$2.elem = 456;
這樣就保證了lambda表達式內(nèi)外訪問到的是同一個對象易桃。
再來看看C#的處理方式褥琐,反編譯一下,發(fā)現(xiàn)C#編譯器生成了如下的一個類:
private sealed class <tryUsingLambda>c__AnonStorey0
{
internal int number;
internal void <>m__0 ()
{
this.number = 456;
Console.WriteLine (this.number);
}
}
把number包裝在這個類內(nèi)晤郑,這樣就保證了lambda表達式內(nèi)外使用的都是同一個number敌呈,即便重新賦值也可以保證內(nèi)外部的數(shù)據(jù)是同步的。
小結(jié)
Scala和C#的編譯器通過把局部變量包裝在另一個對象中造寝,來實現(xiàn)lambda表達式內(nèi)外的數(shù)據(jù)同步磕洪。
而Java的編譯器由于未知的原因(懷疑是為了圖省事兒?)沒有做包裝局部變量這件事兒诫龙,于是就只好強制用戶把局部變量聲明為final才能在匿名內(nèi)部類中使用來避免數(shù)據(jù)不同步的問題析显。