这是速读系列的第4篇文章,内容是一起聊一聊内部匿名类,以及内部匿名类使用外部变量为啥要加final。
小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:对了,我还听说过一个名词,好像叫做:内部匿名类会持有外部对象的引用...这是啥意识?
MDove:呦,都了解的这么多了?关于这个话题,暂时先不开展。后续我会专门好好整理一遍内部,好好帮你理解一下这句话,以及这句话背后所带来的问题。这里先简单写段代码,你可以先看看,解解闷。
public class Main { public static void main(String[] args) { Main main = new Main(); main.fun(); } public void fun() { new FunLisenter() { @Override public void fun() { } }.fun(); } } 复制代码
MDove:直接看,反编译后的匿名内部类:
class Main$1 implements FunLisenter { Main$1(Main var1) { this.this$0 = var1; } public void fun() {} } 复制代码
MDove:OK,这个话题就此打住。以后单聊~接下来继续回答你下一个问题:为啥内部匿名类引用外部变量还得加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:好像还真是这么回事,看样子底层设计真的是一个很有艺术的学问。