内部类引用局部变量与外部类成员变量的问题思考-阿里云开发者社区

开发者社区> 寒凝雪> 正文

内部类引用局部变量与外部类成员变量的问题思考

简介:
+关注继续查看
昨天有一个比较爱思考的同事和我提起一个问题:为什么匿名内部类使用的局部变量和参数需要final修饰,而外部类的成员变量则不用?对这个问题我一直作为默认的语法了,木有仔细想过为什么(在分析完后有点印象在哪本书上看到过,但是就是没有找到,难道是我的幻觉?呵呵)。虽然没有想过,但是还是借着之前研究过字节码的基础上,分析了一些,感觉上是找到了一些答案,分享一下;也希望有大牛给指出一些不足的地方。

  假如我们有以下的代码:

interface Printer {      public void print();  }  class MyApplication {      private int field = 10;       public void print(final Integer param) {          final long local = 100;          final long local2 = param.longValue() + 100;          Printer printer = new Printer() {              @Override              public void print() {                  System.out.println("Local value: " + local);                  System.out.println("Local2 value: " + local2);                  System.out.println("Parameter: " + param);                  System.out.println("Field value: " + field);              }          };          printer.print();      }  }

  这里因为param要在匿名内部类的print()方法中使用,因而它要用final修饰;local/local2是局部变量,因而也需要final修饰;而field是外部类MyApplication的字段,因而不需要final修饰。这种设计是基于什么理由呢?

  我想这个问题应该从Java是如何实现匿名内部类的。其中有两点:

  1、匿名内部类可以使用外部类的变量(局部或成员变来那个)。

  2、匿名内部类中不同的方法可以共享这些变量。

  根据这两点信息我们就可以分析,可能这些变量会在匿名内部类的字段中保存着,并且在构造的时候将他们的值/引用传入内部类。这样就可以保证同时实现上述两点了。

  事实上,Java就是这样设计的,并且所谓匿名类,其实并不是匿名的,只是编译器帮我们命名了而已。这点我们可以通过这两个类编译出来的字节码看出来:

// Compiled from Printer.java (version 1.6 : 50.0, super bit)  class levin.test.anonymous.MyApplication$1 implements levin.test.anonymous.Printer {        // Field descriptor #8 Llevin/test/anonymous/MyApplication;    final synthetic levin.test.anonymous.MyApplication this$0;        // Field descriptor #10 J    private final synthetic long val$local2;        // Field descriptor #12 Ljava/lang/Integer;    private final synthetic java.lang.Integer val$param;        // Method descriptor #14 (Llevin/test/anonymous/MyApplication;JLjava/lang/Integer;)V    // Stack: 3, Locals: 5    MyApplication$1(levin.test.anonymous.MyApplication arg0, long arg1, java.lang.Integer arg2);       0  aload_0 [this]       1  aload_1 [arg0]       2  putfield levin.test.anonymous.MyApplication$1.this$0 : levin.test.anonymous.MyApplication [16]       5  aload_0 [this]       6  lload_2 [arg1]       7  putfield levin.test.anonymous.MyApplication$1.val$local2 : long [18]      10  aload_0 [this]      11  aload 4 [arg2]      13  putfield levin.test.anonymous.MyApplication$1.val$param : java.lang.Integer [20]      16  aload_0 [this]      17  invokespecial java.lang.Object() [22]      20  return        Line numbers:          [pc: 0, line: 1]          [pc: 16, line: 13]        Local variable table:          [pc: 0, pc: 21] local: this index: 0 type: new levin.test.anonymous.MyApplication(){}        // Method descriptor #24 ()V    // Stack: 4, Locals: 1    public void print();       0  getstatic java.lang.System.out : java.io.PrintStream [30]       3  ldc <String "Local value: 100"> [36]       5  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38]       8  getstatic java.lang.System.out : java.io.PrintStream [30]      11  new java.lang.StringBuilder [44]      14  dup      15  ldc <String "Local2 value: "> [46]      17  invokespecial java.lang.StringBuilder(java.lang.String) [48]      20  aload_0 [this]      21  getfield levin.test.anonymous.MyApplication$1.val$local2 : long [18]      24  invokevirtual java.lang.StringBuilder.append(long) : java.lang.StringBuilder [50]      27  invokevirtual java.lang.StringBuilder.toString() : java.lang.String [54]      30  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38]      33  getstatic java.lang.System.out : java.io.PrintStream [30]      36  new java.lang.StringBuilder [44]      39  dup      40  ldc <String "Parameter: "> [58]      42  invokespecial java.lang.StringBuilder(java.lang.String) [48]      45  aload_0 [this]      46  getfield levin.test.anonymous.MyApplication$1.val$param : java.lang.Integer [20]      49  invokevirtual java.lang.StringBuilder.append(java.lang.Object) : java.lang.StringBuilder [60]      52  invokevirtual java.lang.StringBuilder.toString() : java.lang.String [54]      55  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38]      58  getstatic java.lang.System.out : java.io.PrintStream [30]      61  new java.lang.StringBuilder [44]      64  dup      65  ldc <String "Field value: "> [63]      67  invokespecial java.lang.StringBuilder(java.lang.String) [48]      70  aload_0 [this]      71  getfield levin.test.anonymous.MyApplication$1.this$0 : levin.test.anonymous.MyApplication [16]      74  invokestatic levin.test.anonymous.MyApplication.access$0(levin.test.anonymous.MyApplication) : int [65]      77  invokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [71]      80  invokevirtual java.lang.StringBuilder.toString() : java.lang.String [54]      83  invokevirtual java.io.PrintStream.println(java.lang.String) : void [38]      86  return        Line numbers:          [pc: 0, line: 16]          [pc: 8, line: 17]          [pc: 33, line: 18]          [pc: 58, line: 19]          [pc: 86, line: 20]        Local variable table:          [pc: 0, pc: 87] local: this index: 0 type: new levin.test.anonymous.MyApplication(){}    Inner classes:      [inner class info: #1 levin/test/anonymous/MyApplication$1, outer class info: #0       inner name: #0, accessflags: 0 default]    Enclosing Method: #66  #77 levin/test/anonymous/MyApplication.print(Ljava/lang/Integer;)V  }

 

 这些字段在构造函数中赋值,而构造函数则是在MyApplication.print()方法中调用。

  由此,我们可以得出一个结论:Java对匿名内部类的实现是通过编译器来支持的,即通过编译器帮我们产生一个匿名类的类名,将所有在匿名类中用到的局部变量和参数做为内部类的final字段,同是内部类还会引用外部类的实例。其实这里少了local的变量,这是因为local是编译器常量,编译器对它做了替换的优化。

  其实Java中很多语法都是通过编译器来支持的,而在虚拟机/字节码上并没有什么区别,比如这里的final关键字,其实细心的人会发现在字节码中,param参数并没有final修饰,而final本身的很多实现就是由编译器支持的。类似的还有Java中得泛型和逆变、协变等。这是题外话。

  有了这个基础后,我们就可以来分析为什么有些要用final修饰,有些却不用的问题。

  首先我们来分析local2变量,在”匿名类”中,它是通过构造函数传入到”匿名类”字段中的,因为它是基本类型,因而在够着函数中赋值时(撇开对函数参数传递不同虚拟机的不同实现而产生的不同效果),它事实上只是值的拷贝;因而加入我们可以在”匿名类”中得print()方法中对它赋值,那么这个赋值对外部类中得local2变量不会有影响,而程序员在读代码中,是从上往下读的,所以很容易误认为这段代码赋值会对外部类中得local2变量本身产生影响,何况在源码中他们的名字都是一样的,所以我认为了避免这种confuse导致的一些问题,Java设计者才设计出了这样的语法。

  对引用类型,其实也是一样的,因为引用的传递事实上也只是传递引用的数值(简单的可以理解成为地址),因而对param,如果可以在”匿名类”中赋值,也不会在外部类的print()后续方法产生影响。虽然这样,我们还是可以在内部类中改变引用内部的值的,如果引用类型不是只读类型的话;在这里Integer是只读类型,因而我们没法这样做。(如果学过C++的童鞋可以想想常量指针和指针常量的区别)。

  现在还剩下最后一个问题:为什么引用外部类的字段却是可以不用final修饰的呢?细心的童鞋可能也已经发现答案了,因为内部类保存了外部类的引用,因而内部类中对任何字段的修改都回真实的反应到外部类实例本身上,所以不需要用final来修饰它。

  这个问题基本上就分析到这里了,不知道我有没有表达清楚了。

  加点题外话吧。

  首先是,对这里的字节码,其实还有一点可以借鉴的地方,就是内部类在使用外部类的字段时不是直接取值,而是通过编译器在外部类中生成的静态的access$0()方法来取值,我的理解,这里Java设计者想尽量避免其他类直接访问一个类的数据成员,同时生成的access$0()方法还可以被其他类所使用,这遵循了面向对象设计中的两个重要原则:封装和复用。

  另外,对这个问题也让我意识到了即使是语言语法层面上的设计都是有原因可循的,我们要善于多问一些为什么,理解这些设计的原因和局限,记得曾听到过一句话:知道一门技术的局限,我们才能很好的理解这门技术可以用来做什么。也只有这样我们才能不断的提高自己。在解决了这个问题后,我突然冒出了一句说Java这样设计也是合理的。是啊,语法其实就一帮人创建的一种解决某些问题的方案,当然有合理和不合理之分,我们其实不用对它视若神圣。

  之前有进过某著名高校的研究生群,即使在那里,码农论也是甚嚣尘上,其实码农不码农并不是因为程序员这个职位引起的,而是个人引起的,我们要不断理解代码内部的本质才能避免一直做码农的命运那。个人愚见而已,呵呵。


本文出自seven的测试人生公众号最新内容请见作者的GitHub页:http://qaseven.github.io/

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
关于RCP引用jar包的几个问题。
先说明两个概念:类加载器,OSGI 类加载器(class loader),顾名思义就是JVM用于加载类的一个工具。当然,它本身也是一个java类,见java.lang.ClassLoader。它维护内部的类名空间,相互关系(域),并为它们隔绝外部侵害。
676 0
再谈类成员虚函数
以前经常学习虑函数 最多的是当基类指针构造子类对象时,调用函数的应用,但是以前没这一点: 子类对象调用基类成员函数A,然后基类成员函数又会调用一个类成员函数B,这个类成员函数B在子类中也定义,基类中也定义(基类和子类同时拥有)。
734 0
java之路,基础知识----内部类
内部类:在类的内部定义另一个类,方便访问外部类的所有的成员。内部类可以嵌套,但是如果定义在方法之内,那么他的作用域就在方法中,出了方法就不能使用了。必须先产生外部类对象,然后产生内部类对象 class out {     private int index = 100;   ...
595 0
Java 获取类成员方法信息
一、基本的数据类型,void关键字等都存在类类型 Class c = 基类.class (int,String,double,void等) Class c1 = int.class; int的类类型 Class c2 = String.
669 0
shell 全局和局部变量
/******************************************************************** * shell 全局和局部变量 * 声明: * 到目前为止,在写shell脚本的时候,经常被shell的函数中的变量 * 作用域搞得糊涂,于是今天特意查了点资料,看到底怎么回事。
608 0
Java基础-10总结形式参数,包,修饰符,内部类
你需要的是什么,直接评论留言。 获取更多资源加微信公众号“Java帮帮” (是公众号,不是微信好友哦) 还有“Java帮帮”今日头条号,技术文章与新闻,每日更新,欢迎阅读 学习交流请加Java帮帮交流QQ群553841695 分享是一种美德,分享更快乐! 类,抽象类,接口的综合小练习 /* 教练和运动员案例(学生分析然后讲解
1141 0
UNITY3D的变量初始化问题
大部分是代码声明变量,而变量的初始化值在编辑器中指定,不在代码中直接指定 奇怪! 而且只有Public的才能在编辑器中出现,如果你改为Private就不再显示,如果你再改回PUBLIC,原先指定的初化值会丢失!
775 0
+关注
5854
文章
223
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载