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

昨天有一个比较爱思考的同事和我提起一个问题:为什么匿名内部类使用的局部变量和参数需要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/

时间: 2024-09-21 17:29:03

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

内部类引用局部变量为什么要求必须是final的,而引用全局变量就不需要

问题描述 例如在类的方法中定义一个类变量,然后开启一个线程,线程体中引用这个变量,要求必须是final的. 问题补充:<div class="quote_title">zhanjia 写道</div><div class="quote_div">    局部匿名类在源代码编译后也是要生成对应的class文件的(一般会是A$1.class这种形式的文件),那么这个二进制文件是独立于其外围类(A.class)的,就是说它无法知道A类中

java内部类 引用外部类对象this问题

问题描述 java内部类 引用外部类对象this问题 在内部类里,想引用外部类的对象,为什么是外部类.this而不是外部类加引用变量,还有.操作应该是成员操作符吧,这样this不就成了外部类成员了 解决方案 你好,实际上我们代指当前类对象的this是个简写形式,完整的形式就是 类名字.this,举个例子来说吧 class OuterClass { private String outAttribute = "我是外部类属性"; public void printOutAttribute

java 内部类-为什么外部类能访问内部类的私有成员?

问题描述 为什么外部类能访问内部类的私有成员? 在学习单例时发现,外部类可以访问内部类的私有成员 如下面代码所示: //Initialization on Demand Holder class Singleton { private Singleton() { } private static class HolderClass { private final static Singleton instance = new Singleton(); } public static Single

构造方法 数组-关于成员变量和局部变量

问题描述 关于成员变量和局部变量 堆中的成员变量变成引用后会跑到栈中么,创建一个引用型对象,调用构造方法,变量是局部变量么 解决方案 不会 引用还绑定的还是堆变量 解决方案二: 堆与栈是两种不同的结构.栈一般又可成为函数栈,用来存储函数的局部变量以及被调用者寄存器的值(如果用到),还有传给子程序的参数等等. 栈在编译时便已经分配好,而堆(heap)确是在运行中动态创建的. 堆与栈分属不同的内存空间,没有直接的联系.在C语言中用malloc动态分配内存,这就是堆操作,在C++中用new动态分派内存

java中成员变量与局部变量区别分析_java

本文实例分析了java中成员变量与局部变量区别.分享给大家供大家参考.具体分析如下: 成员变量:在这个类里定义的私有变量,属于这个类. 创建以及使用成员变量 复制代码 代码如下: public class Person {     String name;     String Sex;     int age;     double Height;         public static void main(String arges[])     {         Person p=ne

浅谈静态变量、成员变量、局部变量三者的区别_java

静态变量和成员变量的区别: A:所属不同  静态变量:属于类,类变量    成员变量:属于对象,对象变量,实例变量 B:内存位置不同  静态变量:方法区的静态区    成员变量:堆内存 C:生命周期不同  静态变量:静态变量是随着类的加载而加载,随着类的消失而消失    成员变量:成员变量是随着对象的创建而存在,随着对象的消失而消失 D:调用不同  静态变量:可以通过对象名调用,也可以通过类名调用    成员变量:只能通过对象名调用 成员变量和局部变量的区别: A:在类中的位置不同  成员变量:

如何区分成员变量和局部变量

问题描述 如何区分成员变量和局部变量 一直分不清成员变量和局部变量?????????????????????????????????????????????????????????????????????? 解决方案 成员变量定义在类里面,函数外面,局部变量定义在函数里面. 一般的开发环境(VS VC++ Eclipse等)你要区分,可以光标选择变量,右键,点转到定义,然后看在哪里定义的. 解决方案二: 成员变量:又叫类变量,全局变量,定义在类里面 局部变量:定义在方法里面,OnCreate()

浅析Java中局部变量与成员变量同名解决技巧_java

要想区分这哥俩,首先,我们得知道它们分别是什么.先从成员变量下刀. 成员变量        我们来研究一个事物:                属性:外在特征:例如人的身高,体重                行为:能够做什么:例如人有说话,打球等行为.        而在Java语言中,最基本的单位是类,类就是用来体现事物的.        用类class来描述事物也是如此:                属性:对应类中的成员变量                行为:对应类中的成员函数    

java-在本类中调用自己的成员变量时,变量前也要加this是为什么,其作用是什么

问题描述 在本类中调用自己的成员变量时,变量前也要加this是为什么,其作用是什么 java在本类中使用this指针的作用,感觉加不加this指针运行结果都一样啊,这是怎么回事(在学习写坦克大战项目时,突然想到的问题,求大神教一教) 解决方案 一般来说,this可以省略.可写可不写 有种情况例外: 构造函数 class A { public int x; public A(int x) { this.x = x; //这里为了区分两个都叫x的变量,就要写 } } 解决方案二: 其实也不仅构造函数