为什么说Java匿名内部类是残缺的闭包

https://blog.csdn.net/hzy38324/article/details/77986095

前言

我们先来看一道很简单的小题:

public class AnonymousDemo1
{public static void main(String args[]) { new AnonymousDemo1().play(); } private void play() { Dog dog = new Dog(); Runnable runnable = new Runnable() { public void run() { while(dog.getAge()<100) { // 过生日,年龄加一 dog.happyBirthday(); // 打印年龄 System.out.println(dog.getAge()); } } }; new Thread(runnable).start(); // do other thing below when dog's age is increasing // .... } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

其中Dog类是这样的:

public class Dog
{private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public void happyBirthday() { this.age++; } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

这段程序的功能非常简单,就是启动一个线程,来模拟一只小狗不断过生日的一个过程。

不过,这段代码并不能通过编译,为什么,仔细看一下! 






看出来了吗?是的,play()方法中,dog变量要加上final修饰符,否则会提示:

Cannot refer to a non-final variable dog inside an inner class defined in a different method

加上final后,编译通过,程序正常运行。 
但是,这里为什么一定要加final呢? 
学Java的时候,我们都听过这句话(或者类似的话):

匿名内部类来自外部闭包环境的自由变量必须是final的

那时候一听就懵逼了,什么是闭包?什么叫自由变量?最后不求甚解,反正以后遇到这种情况就加个final就好了。 
显然,这种对待知识的态度是不好的,必须“知其然并知其所以然”,最近就这个问题做了一番研究,希望通过比较通俗易懂的言语分享给大家。

我们学框架、看源码、学设计模式、学并发编程、学缓存,甚至了解大型网站架构设计,可回过头来看看一些非常简单的Java代码,却发现还有那么几个旮旯,是自己没完全看透的。

匿名内部类的真相

既然不加final无法通过编译,那么就加上final,成功编译后,查看class文件反编译出来的结果。 
在class目录下面,我们会看到有两个class文件:AnonymousDemo1.class和AnonymousDemo1$1.class,其中,带美元符号$的那个class,就是我们代码里面的那个匿名内部类。接下来,使用 jd-gui 反编译一下,查看这个匿名内部类:

class AnonymousDemo1$1implements Runnable
{AnonymousDemo1$1(AnonymousDemo1 paramAnonymousDemo1, Dog paramDog) {}public void run() { while (this.val$dog.getAge() < 100) { this.val$dog.happyBirthday(); System.out.println(this.val$dog.getAge()); } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这代码看着不合常理:

  • 首先,构造函数里传入了两个变量,一个是AnonymousDemo1类型的,另一个是Dog类型,但是方法体却是空的,看来是反编译时遗漏了;
  • 再者,run方法里this.val$dog这个成员变量并没有在类中定义,看样子也是在反编译的过程中遗漏掉了。

既然 jd-gui 的反编译无法完整的展示编译后的代码,那就只能使用 javap 命令来反汇编了,在命令行中执行:

javap -c AnonymousDemo1$1.class
  • 1

执行完命令后,可以在控制台看到一些汇编指令,这里主要看下内部类的构造函数:

com.bridgeforyou.anonymous.AnonymousDemo1$1(com.bridgeforyou.anonymous.Anonymo usDemo1, com.bridgeforyou.anonymous.Dog); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field this$0:Lcom/bridgeforyou/an onymous/AnonymousDemo1; 5: aload_0 6: aload_2 7: putfield #16 // Field val$dog:Lcom/bridgeforyou/a nonymous/Dog; 10: aload_0 11: invokespecial #18 // Method java/lang/Object."<init>": ()V 14: return
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这段指令的重点在于第二个putfield指令,结合注释,我们可以知道,构造器函数将传入的dog变量赋值给了另一个变量,现在,我们可以手动填补一下上面那段信息遗漏掉的反编译后的代码:

class AnonymousDemo1$1implements Runnable
{private Dog val$dog;private AnonymousDemo1 myAnonymousDemo1;AnonymousDemo1$1(AnonymousDemo1 paramAnonymousDemo1, Dog paramDog) {this.myAnonymousDemo1 = paramAnonymousDemo1; this.val$dog = paramDog; } public void run() { while (this.val$dog.getAge() < 100) { this.val$dog.happyBirthday(); System.out.println(this.val$dog.getAge()); } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

至于外部类AnonymousDemo1,则是把dog变量传递给AnonymousDemo1$1的构造器,然后创建一个内部类的实例罢了,就像这样:

public class AnonymousDemo1
{public static void main(String[] args) { new AnonymousDemo1().play(); } private void play() { final Dog dog = new Dog(); Runnable runnable = new AnonymousDemo1$1(this, dog); new Thread(runnable).start(); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

关于Java汇编指令,可以参考 Java bytecode instruction listings

到这里我们已经看清匿名内部类的全貌了,其实Java就是把外部类的一个变量拷贝给了内部类里面的另一个变量。 
我之前在 用画小狗的方法来解释Java值传递 这篇文章里提到过,Java里面的变量都不是对象,这个例子中,无论是内部类的val$dog变量,还是外部类的dog变量,他们都只是一个存储着对象实例地址的变量而已,而由于做了拷贝,这两个变量指向的其实是同一只狗(对象)。

那么为什么Java会要求外部类的dog一定要加上final呢? 
一个被final修饰的变量:

  • 如果这个变量是基本数据类型,那么它的值不能改变;
  • 如果这个变量是个指向对象的引用,那么它所指向的地址不能改变。

关于final,维基百科说的非常清楚 final (Java) - Wikipedia

因此,这个例子中,假如我们不加上final,那么我可以在代码后面加上这么一句dog = new Dog(); 就像下面这样:

// ...
new Thread(runnable).start();// do other thing below when dog's age is increasing
dog = new Dog();
  • 1
  • 2
  • 3
  • 4
  • 5

这样,外面的dog变量就指向另一只狗了,而内部类里的val$dog,还是指向原先那一只,就像这样:

这样做导致的结果就是内部类里的变量和外部环境的变量不同步,指向了不同的对象。 
因此,编译器才会要求我们给dog变量加上final,防止这种不同步情况的发生。

为什么要拷贝

现在我们知道了,是由于一个拷贝的动作,使得内外两个变量无法实时同步,其中一方修改,另外一方都无法同步修改,因此要加上final限制变量不能修改。

那么为什么要拷贝呢,不拷贝不就没那么多事了吗?

这时候就得考虑一下Java虚拟机的运行时数据区域了,dog变量是位于方法内部的,因此dog是在虚拟机栈上,也就意味着这个变量无法进行共享,匿名内部类也就无法直接访问,因此只能通过值传递的方式,传递到匿名内部类中。

那么有没有不需要拷贝的情形呢?有的,请继续看。

一定要加final吗

我们已经理解了要加final背后的原因,现在我把原来在函数内部的dog变量,往外提,“提拔”为类的成员变量,就像这样:

public class AnonymousDemo2
{private Dog dog = new Dog(); public static void main(String args[]) { new AnonymousDemo2().play(); } private void play() { Runnable runnable = new Runnable() { public void run() { while (dog.getAge() < 100) { // 过生日,年龄加一 dog.happyBirthday(); // 打印年龄 System.out.println(dog.getAge()); } } }; new Thread(runnable).start(); // do other thing below when dog's age is increasing // .... } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

这里的dog成了成员变量,对应的在虚拟机里是在堆的位置,而且无论在这个类的哪个地方,我们只需要通过 this.dog,就可以获得这个变量。因此,在创建内部类时,无需进行拷贝,甚至都无需将这个dog传递给内部类。

通过反编译,可以看到这一次,内部类的构造函数只有一个参数:

class AnonymousDemo2$1implements Runnable
{AnonymousDemo2$1(AnonymousDemo2 paramAnonymousDemo2) {}public void run() { while (AnonymousDemo2.access$0(this.this$0).getAge() < 100) { AnonymousDemo2.access$0(this.this$0).happyBirthday(); System.out.println(AnonymousDemo2.access$0(this.this$0).getAge()); } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在run方法里,是直接通过AnonymousDemo2类来获取到dog这个对象的,结合javap反汇编出来的指令,我们同样可以还原出代码:

class AnonymousDemo2$1implements Runnable
{private AnonymousDemo2 myAnonymousDemo2;AnonymousDemo2$1(AnonymousDemo2 paramAnonymousDemo2) {this.myAnonymousDemo2 = paramAnonymousDemo2;}public void run() { while (this.myAnonymousDemo2.getAge() < 100) { this.myAnonymousDemo2.happyBirthday(); System.out.println(this.myAnonymousDemo2.getAge()); } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

相比于demo1,demo2的dog变量具有”天然同步”的优势,因此就无需拷贝,因而编译器也就不要求加上final了。

回看那句经典的话

上文提到了这句话 —— “匿名内部类来自外部闭包环境的自由变量必须是final的”,一开始我不理解,所以看着很蒙圈,现在再来回看一下:

首先,自由变量是什么? 
一个函数的“自由变量”就是既不是函数参数也不是函数内部局部变量的变量,这种变量一般处于函数运行时的上下文,就像demo中的dog,有可能第一次运行时,这个dog指向的是age是10的狗,但是到了第二次运行时,就是age是11的狗了。

然后,外部闭包环境是什么? 
外部环境如果持有内部函数所使用的自由变量,就会对内部函数形成“闭包”,demo1中,外部play方法中,持有了内部类中的dog变量,因此形成了闭包。 
当然,demo2中,也可以理解为是一种闭包,如果这样理解,那么这句经典的话就应该改为这样更为准确:

匿名内部类来自外部闭包环境的自由变量必须是final的,除非自由变量来自类的成员变量。

对比JavaScript的闭包

从上面我们也知道了,如果说Java匿名内部类时一种闭包的话,那么这是一种有点“残缺”的闭包,因为他要求外部环境持有的自由变量必须是final的。

而对于其他语言,比如C#和JavaScript,是没有这种要求的,而且内外部的变量可以自动同步,比如下面这段JavaScript代码(运行时直接按F12,在打开的浏览器调试窗口里,把代码粘贴到Console页签,回车就可以了):

function fn() {var myVar = 42; var lambdaFun = () => myVar; console.log(lambdaFun()); // print 42 myVar++; console.log(lambdaFun()); // print 43 } fn();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这段代码使用了lambda表达式(Java8也提供了,后面会介绍)创建了一个函数,函数直接返回了myVar这个外部变量,在创建了这个函数之后,对myVar进行修改,可以看到函数内部的变量也同步修改了。 
应该说,这种闭包,才是比较“正常“和“完整”的闭包。

Java8之后的变动

在JDK1.8中,也提供了lambda表达式,使得我们可以对匿名内部类进行简化,比如这段代码:

int answer = 42;
Thread t = new Thread(new Runnable() {public void run() { System.out.println("The answer is: " + answer); } });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

使用lambda表达式进行改造之后,就是这样:

int answer = 42;
Thread t = new Thread(() -> System.out.println("The answer is: " + answer)
);
  • 1
  • 2
  • 3
  • 4

值得注意的是,从JDK1.8开始,编译器不要求自由变量一定要声明为final,如果这个变量在后面的使用中没有发生变化,就可以通过编译,Java称这种情况为“effectively final”。 
上面那个例子就是“effectively final”,因为answer变量在定义之后没有变化,而下面这个例子,则无法通过编译:

int answer = 42;
answer ++; // don't do this !
Thread t = new Thread(() -> System.out.println("The answer is: " + answer) );
  • 1
  • 2
  • 3
  • 4
  • 5

花絮

在研究这个问题时,我在StackOverflow参考了这个问题:Cannot refer to a non-final variable inside an inner class defined in a different method

其中一个获得最高点赞、同时也是被采纳的回答,是这样解释的:

When the main() method returns, local variables (such as lastPrice and price) will be cleaned up from the stack, so they won’t exist anymore after main() returns. 
But the anonymous class object references these variables. Things would go horribly wrong if the anonymous class object tries to access the variables after they have been cleaned up. 
By making lastPrice and price final, they are not really variables anymore, but constants. The compiler can then just replace the use of lastPrice and price in the anonymous class with the values of the constants (at compile time, of course), and you won’t have the problem with accessing non-existent variables anymore.

大致的意思是:由于外部的变量会在方法结束后被销毁,因此要将他们声明为final常量,这样即使外部类的变量销毁了,内部类还是可以使用。

这么浅显、无根无据的解释居然也获得了那么多赞,后来评论区有人指出了错误,回答者才在他的回答里加了一句:

edit - See the comments below - the following is not a correct explanation, as KeeperOfTheSoul points out.

可见,看待一个问题时,不能只从表面去解释,要解释一个问题,必须弄清背后的原理。

转载于:https://www.cnblogs.com/handsome1013/p/9415566.html

为什么说Java匿名内部类是残缺的闭包相关推荐

  1. 去除残缺条目java_为什么说Java匿名内部类是残缺的闭包

    为什么说Java匿名内部类是残缺的闭包 https://blog.csdn.net/hzy38324/article/details/77986095 前言 我们先来看一道很简单的小题: public ...

  2. Java Lambda 表达式(又名闭包 (Closure)/ 匿名函数 ) 笔记

    Java Lambda 表达式(又名闭包 (Closure)/ 匿名函数 ) 笔记 根据 JSR 335, Java 终于在 Java 8 中引入了 Lambda 表达式.也称之为闭包或者匿名函数. ...

  3. Java:伪造工厂的闭包以创建域对象

    最近,我们想要创建一个域对象,该对象需要具有外部依赖关系才能进行计算,并且希望能够在测试中解决该依赖关系. 最初,我们只是在领域类中新建依赖项,但这使得无法在测试中控制其值. 同样,我们似乎不应该将这 ...

  4. java 匿名内部类 百科_java匿名内部类具体概念是什么,在什么地方用到?

    展开全部 java匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类,没有类名,根据多态,我们e69da5e887aa62616964757a686964616f313333376138 ...

  5. Java匿名内部类里为什么能用外部变量

    2019独角兽企业重金招聘Python工程师标准>>> 偶然间想到一个问题,Java匿名内部类里为什么能用外部变量?Java到底在背后做了什么呢: final List<Int ...

  6. 关于JAVA匿名内部类,回调,事件模式的一点讨论

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 关于JA ...

  7. java匿名内部类,什么是匿名内部类,如何定义匿名内部类,如何使用匿名内部类?

    java匿名内部类 什么是匿名内部类? 匿名内部类的使用场景? 匿名内部类如何创建? 匿名内部类的实现和使用 例1(实现接口) 例2(继承类) 什么是匿名内部类? 匿名内部类,顾名思义,就是不知道这个 ...

  8. Java匿名内部类的用法(简单教学)

    Java匿名内部类笔记 public class Test{public static void main(String[] args){/*语法*///Coo o1 = new Coo(); //创 ...

  9. java 匿名内部类 参数_Java匿名内部类原理与用法详解

    本文实例讲述了Java匿名内部类原理与用法.分享给大家供大家参考,具体如下: 一 点睛 匿名内部类适合创建那种只需要一次使用的类,定义匿名内部类的语法格式如下: new 父类构造器(实参列表) | 实 ...

最新文章

  1. hbid新建html标签不能用,hbhdjtx.html
  2. 【错误记录】发布 Flutter 插件包报错 ( It‘s strongly recommended to include a “homepage“ or “repository“ field )
  3. (视频+图文)机器学习入门系列-第2章 线性回归
  4. Python | 深入浅出字符串
  5. redis nosql_Redis教程:NoSQL键值存储
  6. Qt之设置QWidget背景色
  7. float和position
  8. LeetCode 103. Binary Tree Zigzag Level Order Traversal
  9. Mac工具PullTube如何在下载列表中创建重复项
  10. 手机mtkcdc端口如何开启_MTK手机连接电脑说明书
  11. 作业5-需求分析(EX:南通大学成绩录入系统)
  12. 台式机机械硬盘 - 简单快捷的安装
  13. 《大话脑成像》之Linux基础命令
  14. Java面向对象和面向过程的区别
  15. cadence 通孔焊盘_allegro软件通孔类焊盘制作方法及步骤
  16. NZT 关于触动精灵 扫码无法识别 NZT提示202
  17. 算法笔记_227:填写乘法算式(Java)
  18. 插件推荐:json解析—Gson以及GsonFormat插件的运用
  19. [Appium]MAC安装Appium
  20. Thread协议介绍

热门文章

  1. Odoo产品分析 (三) -- 人力资源板块(4) -- 招聘流程(1)
  2. java毕业设计大学生创业众筹系统mybatis+源码+调试部署+系统+数据库+lw
  3. python 实现 享元模式
  4. java开源b2b2c商城系统_java开源b2b2c商城系统有好用的吗?
  5. docker之容器常用命令及基本操作
  6. 后渗透利用sethc留下后门
  7. excel画图如何添加图表数据参考线
  8. WSL嵌入式开发系列教程 1 —— 安装指南
  9. 火狐浏览器中如何设置自动翻译网页
  10. 通过各种统计方法建立理想的mlb投球前景