这是速读系列的第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:好像还真是这么回事,看样子底层设计真的是一个颇有艺术的学问。