這是速讀系列的第3篇文章,內容是一塊兒聊一聊內部匿名類,以及內部匿名類使用外部變量爲啥要加final。java
愛因斯坦:「若是你不能簡單地解釋同樣東西,說明你沒真正理解它。」面試
[短文速讀-2] 重載/重寫,動/靜態分派?(從新修訂)安全
[短文速讀-3] 內部匿名類使用外部變量爲何要加final多線程
[短文速讀 -5] 多線程編程引子:進程、線程、線程安全post
小A:MDove,我最近在學習匿名內部類的時候有點懵逼了?咋還起了個這麼洋氣的名字?啥是內部匿名類啊?爲啥它引用外部變量還得加final?還不能從新賦值?學習
MDove:哎呦,叭叭的,問題還挺多。話說回來,內部匿名類的確是一個很彆扭的存在。那我們今天就好好聊一聊內部匿名類,好好從源頭解一解你的疑問。this
MDove:我們先寫一個普通的內部匿名類的簡單demo:spa
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呢?
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:好像還真是這麼回事,看樣子底層設計真的是一個頗有藝術的學問。