前言
這是速讀系列的第3篇文章,內容是一起聊一聊內部匿名類,以及內部匿名類使用外部變數為啥要加final。
愛因斯坦:“如果你不能簡單地解釋一樣東西,說明你沒真正理解它。”
[短文速讀-3] 內部匿名類使用外部變數為什麼要加final
[短文速讀 -5] 多執行緒程式設計引子:程式、執行緒、執行緒安全
引子
小A:MDove,我最近在學習匿名內部類的時候有點懵逼了?咋還起了個這麼洋氣的名字?啥是內部匿名類啊?為啥它引用外部變數還得加final?還不能重新賦值?
MDove:哎呦,叭叭的,問題還挺多。話說回來,內部匿名類的確是一個很彆扭的存在。那我們們今天就好好聊一聊內部匿名類,好好從源頭解一解你的疑問。
啥是內部匿名類
MDove:我們們先寫一個普通的內部匿名類的簡單demo:
public class Main {
public static void main(String[] args) {
final String name = "Haha";
new FunLisenter() {
@Override
public void fun() {
System.out.println(name);
}
}.fun();
}
}
// 外部介面
public interface FunLisenter {
void fun();
}
複製程式碼
MDove:先解答你第一個問題,啥是內部匿名類。上述demo中的:
new FunLisenter() {
@Override
public void fun() {
System.out.println(name);
}
}
複製程式碼
這就是內部匿名類。
小A:啊?它不就是普通的new麼?咋還成內部匿名類了?
MDove:它就是普通的new?!!!你怎麼學的Java!!介面能被new麼!大聲告訴我,介面能被new麼?!
小A:不能!...不...能...能吧?這不new出來了...
MDove:介面不能new!為什麼這裡被new出來了?因為它是匿名內部類,它是特殊的存在!
小A:(小聲嗶嗶...)特殊在哪?
MDove:Java語言規定,介面不能被new!既然這是“甲魚的臀部”,那麼new FunLisenter()...就一定不是我們表面上看到的new!介面!!不給你扯犢子,直接上編譯後的.class檔案:
MDove:瞪大你的眼,仔細看!有什麼不同?
小A:咦?怎麼2個java檔案編譯後出來了3個class檔案?
MDove:這就是特殊的存在,我們反編譯這個特別的Main$1.class檔案:
final class Main$1 implements FunLisenter {
Main$1() {
}
public void fun() {
System.out.println("Haha");
}
}
複製程式碼
MDove:這很清晰吧?看明白了麼?
小A:嗯??一個奇怪的類實現了我們的FunLisenter介面??難道我new的FunLisenter就是new的這個奇怪的Main$1類麼?
MDove:呦,這麼快反應過來了?再深入思考一下。為啥叫做匿名,是不是有點感覺了?對於我們java層面來說,這個類壓根就看不到。
小A:那它為啥要叫內部類呀?
MDove:啊?Main$1,不叫內部類叫啥類?你有沒有編譯過含有內部類的類?內部類的class檔案就是這樣啊!
小A:哦哦,好像還真是這樣!那也就是說之所以被稱之為內部匿名類,是因為:在編譯階段,編譯器幫我們以內部類的形式,幫我們implement我們的介面,因此我們才可以以new的方式使用。
MDove:沒錯,你理解的很到位。
小A:內部匿名類我明白了,那為啥加final呢?
內部匿名類引用外部變數為什麼得加final
MDove:我們們改寫一段簡單的程式碼:
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.fun();
}
public void fun() {
// 這裡為什麼賦值為null,因為避免String常量對效果的影響
final String nameInner = null;
lisenter = new FunLisenter() {
@Override
public void fun() {
System.out.println(nameInner);
}
}.fun();
}
}
複製程式碼
MDove:首先,我們們先對這幾行程式碼,先提一個問題:為什麼內部匿名類能夠訪問到nameInner?一個方法,就是一個棧幀。對於區域性變數來說,方法結束,棧幀彈出,區域性變數煙消雲散。那麼為什麼內部匿名類可以訪問?
小A:對啊,為什麼?
MDove:你來給我捧哏的?我問你問題呢?
小A:(小聲嗶嗶)...我不知道啊。
MDove:讓我們直接看反編譯的class檔案:
class Main$1 implements FunLisenter {
Main$1(Main var1, String var2) {
this.this$0 = var1;
this.val$nameInner = var2;
}
public void fun() {
System.out.println(this.val$nameInner);
}
}
複製程式碼
MDove:不用解釋了吧?這個例子不光解釋了內部匿名類為什麼能夠訪問區域性變數,還展示了持有外部引用的問題。區域性變數nameInner,被我們的編譯期在生成匿名內部類的時候以引數的形式賦值給了我們內部持有的外部變數了。因此我們呼叫fun()方法時,就直接使用this.val$nameInner。
小A:原來是這樣...那為啥一定要加final呢?
MDove:其實這很好理解,首先問你一個問題。從java程式碼上來看區域性變數nameInner和匿名內部類的nameInner是同一個物件麼?
小A:那還用問麼!當然是一個啦...
MDove:沒錯,從外部看,它的確是同一個。但是我們也反編譯了位元組碼,發現這二者並非是同一個物件。我們們設想一下:如果我們不加final。在Java的這種設計下,一定會造成這種情況:我們在內部匿名類中重新賦值,但是區域性變數並不會同步發生變化。因為按照這種設計,重新賦值,完全就是倆個變數!因此為了避免這種情況,索性加上了final。修改值不同步?連修改都不能修改,還需要什麼同步!
其他語言的設計方案
小A:感覺是一個很彆扭的設計?其他語言也是這樣麼?
MDove:你別說,其他語言還真不是這樣。比如同為面嚮物件語言的C#:C#在編譯過程中隱式的把變數包裝在一個類裡邊。因此就可以避免修改不同步的問題。接下來我們用Java模擬一下這種方案:
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.fun();
}
public void fun() {
final TempModel tempModel = new TempModel("Haha");
System.out.println(tempModel.name);
new FunLisenter() {
@Override
public void fun() {
System.out.println(tempModel.name);
tempModel.name = "Hehe";
}
}.fun();
System.out.println(tempModel.name);
}
}
public class TempMain {
private String name;
public TempMain(String name) {
this.name = name;
}
}
複製程式碼
MDove:我們用簡單的一個物件,包裝了我們想使用的變數。這樣就達到了,不用final的效果。
小A:TempModel也加final了呀?
MDove:加final那是因為Java語言的規定,你仔細想想,這是一個物件。加不加final會對內部的值造成影響麼?這也就是C#實現區域性變數的原理。
小A:好像還真是這麼回事,看樣子底層設計真的是一個很有藝術的學問。