黑马程序员技术交流社区

标题: 为什么内部类使用外部类的变量必须是final类型的 [打印本页]

作者: 周朋飞    时间: 2012-6-27 09:59
标题: 为什么内部类使用外部类的变量必须是final类型的
本帖最后由 周朋飞 于 2012-6-28 11:32 编辑

  在了解动态代理的时候,当在InvocationHander内部的实现类中 使用外部类的参数或者变量为什么必须是final类型的
作者: 吴琼    时间: 2012-6-27 10:21
原因如下:

abstract class ABSClass{
public abstract void print();
}

public class Test2{
public static void test(final String s){//一旦参数在匿名类内部使用,则必须是final
ABSClass c=new ABSClass(){
public void print(){
System.out.println(s);
}
};
c.print();
}
public static void main(String[] args){
test("Hello World!");
}
}

JVM中每个进程都会有多个根,每个static变量,方法参数,局部变量,当然这都是指引用类型.基础类型是不能作为根的,根其实就是一个存储地址.垃圾回收器在工作时先从根开始遍历它引用的对象并标记它们,如此递归到最末梢,所有根都遍历后,没有被标记到的对象说明没有被引用,那么就是可以被回收的对象(有些对象有finalized方法,虽然没有引用,但JVM中有一个专门的队列引用它们直到finalized方法被执行后才从该队列中移除成为真正没有引用的对象,可以回收,这个与本主题讨论的无关,包括代的划分等以后再说明).这看起来很好.

但是在内部类的回调方法中,s既不可能是静态变量,也不是方法中的临时变量,也不是方法参数,它不可能作为根,在内部类中也没有变量引用它,它的根在内部类外部的那个方法中,如果这时外面变量s重指向其它对象,则回调方法中的这个对象s就失去了引用,可能被回收,而由于内部类回调方法大多数在其它线程中执行,可能还要在回收后还会继续访问它.这将是什么结果?

而使用final修饰符不仅会保持对象的引用不会改变,而且编译器还会持续维护这个对象在回调方法中的生命周期.所以这才是final变量和final参数的根本意义
作者: sbeeqnui1987    时间: 2012-6-27 11:06
     首先方法中定义内部类,是为了控制这个类的可见性,编译之后,也会出现外部类和内部类各自的字节码,他们有各自的成员变量和方法。
其次方法和内部类的生命周期不同:前者执行完毕,方法内局部变量也会随之销毁,而此时再让内部类访问显然是不合理的。
而方法中的变量前加final,当某一属性加此关键词后就成为常量了,而常量的生命周期为程序的整个执行期间
所以方法内的类只能访问带final的局部变量

内部类并不是直接调用方法传进来的参数,而是内部类将传进来的参数通过自己的构造器备份到了自己的内部,自己内部的方法调用的实际是自己的属性而不是外部类方法的参数。
这样理解就很容易得出为什么要用final了,因为两者从外表看起来是同一个东西,实际上却不是这样,如果内部类改掉了这些参数的值也不可能影响到原参数,然而这样却失去了参数的一致性,因为从编程人员的角度来看他们是同一个东西,如果编程人员在程序设计的时候在内部类中改掉参数的值,但是外部调用的时候又发现值其实没有被改掉,这就让人非常的难以理解和接受,为了避免这种尴尬的问题存在,所以编译器设计人员把内部类能够使用的参数设定为必须是final来规避这种莫名其妙错误的存在
作者: 孙浩迪    时间: 2012-6-27 14:46
是变量的作用域的问题,因为匿名内部类是出现在一个方法的内部的,如果它要访问这个方法的参数或者方法中定义的变量,则这些参数和变量必须被修饰为final。因为虽然匿名内部类在方法的内部,但实际编译的时候,内部类编译成Outer.Inner,这说明内部类所处的位置和外部类中的方法处在同一个等级上,外部类中的方法中的变量或参数只是方法的局部变量,这些变量或参数的作用域只在这个方法内部有效。因为编译的时候内部类和方法在同一级别上,所以方法中的变量或参数只有为final,内部类才可以引用。

Java代码:
package com.cxz.j2se;   
  
public class MyClass {   
    public MyClass() {   
        final int finalValue = 10;   
        int not$Final = 20;   
        MyInterface myInterface = new MyInterface() {   
            public void functionWithoutPara() {   
                //compile Error   
                //System.out.println(noFinal);   
                System.out.println(finalValue);   
            }   
  
            public void functionWithPara(int num) {   
                System.out.println("The parameter " + num   
                        + " has been passed by the method");   
            }   
  
        };   
        myInterface.functionWithoutPara();   
        myInterface.functionWithPara(not$Final);   
        System.out.println(myInterface.getClass().getName());   
    }   
  
    public static void main(String[] args) {   
        new MyClass();   
  
    }   
  
}  

二、为什么局部内部类只能访问final变量
简单的来说是作用域的问题。就好像方法外面做的事情并不能改变方法内才定义的变量,因为你并不知道方法里面这个时候已经存在了这个局部变量了没有。在这个内部类中方法里面的本地变量是失效的,也就是不在作用域内,所以是不能够访问的

但是为什么这里用final却又可以访问呢?
因为Java采用了一种copy   local   variable的方式来实现,也就是说把定义为final的局部变量拷贝过来用,而引用的也可以拿过来用,只是不能重新赋值。从而造成了可以access   local   variable的假象,而这个时候由于不能重新赋值,所以一般不会造成不可预料的事情发生

作者: 吴林军    时间: 2012-6-27 15:56
本帖最后由 吴林军 于 2012-6-27 16:18 编辑

定义带参数的局部类的方法结束的时候,内部类才刚开始运行,局部变量才开始使用。
但是局部变量会因为方法的不再使用而消失。
所以内部类在使用外部类的变量时必须用final类型的,以保证局部变量与在局部类建立时使用时一致!
否者会影响程序的运行!
作者: 黄奕豪    时间: 2012-6-27 23:24
这个问题我在论坛里面看了N 遍~~~~~~~~请找一下类似帖子!同学!
作者: 周朋飞    时间: 2012-6-28 10:36
嗯 已经明白了
作者: 刘蕴学    时间: 2012-6-28 15:26
本帖最后由 刘蕴学 于 2012-6-28 15:28 编辑

还得我亲自出马。。。另外就是引用类型和基本类型在这里的表现是不一样的,你区分他们的话最好拿你自己定义的类来测试。

代码你自己跑吧,看不懂的话,我也木有办法了


  1. import java.lang.reflect.Field;


  2. public class NoFinalFieldCnnotUsedInner
  3. {
  4.         public static void main(String[] args)
  5.         {
  6.                 Outer outer = new Outer();
  7.                 SuperInner inner = outer.outer();
  8. //                inner.inner();
  9.         }
  10. }
  11. /* 定义一个局部内部类的上层接口,用以允许返回局部内部类实例 */
  12. interface SuperInner
  13. {
  14.         void inner();
  15. }
  16. /* 定义一个外部类,包括一个outer方法,用以返回局部内部类实例 */
  17. class Outer
  18. {
  19.         int y = 0;
  20.         //out方法将返回inner实例
  21.         SuperInner outer()
  22.         {
  23.                 //假设我们这里x可以被inner访问,那么outer函数返回之后
  24.                 //x已经被销毁,inner对象就无法访问x
  25.                
  26.                 //换句话说就是Inner对象的生存周期超出了x的边界,而能保证
  27.                 //生存周期一致的情况只有inner对象自己的成员,所以通过在
  28.                 //inner里边复制一个x来保存这个局部变量x的值以达到目的
  29.                 //就像val$x一样
  30.                
  31.                 //但这种办法有个致命点,如果在inner里对val$x进行赋值操作
  32.                 //外边的x并不会被改变,所以为了保证两个对象的一致性,该变量
  33.                 //x以及其复制变量val$x必须为终态的,也就是final,不能再次
  34.                 //赋值
  35.                
  36.                 //反编译inner可以证明以上结论,在字节码里
  37. //                question6.Outer$1Inner(question6.Outer, java.lang.String)
  38.                 //这是构造方法,传outer不奇怪,内部类都需要外部类的引用指针
  39.                 //这个String字符串就值得思考了
  40.                
  41.                 //4 getfield val$y
  42.                 //在inner方法中可以看到这个字节码,是val$y 而不是y
  43.                 int x = 4;
  44.                 final String y = new String("asd");
  45.                 class Inner implements SuperInner
  46.                 {
  47. //                        int val$x = 4;
  48.                         public void inner()
  49.                         {
  50.                                 try
  51.                                 {
  52.                                         //这里通过反射,可以发现证明此y非彼y,因为我们并没有
  53.                                         //在inner里定义val$y变量,而我们却可以拿得到,没异常
  54.                                         //而我们改的是val$y的值为qqq,下边的syso(y)居然会输出
  55.                                         //qqq,结论可以证实
  56.                                        
  57.                                         //通过代码也可以看出来,这个val$y肯定是private的,并且
  58.                                         //他也是final的
  59.                                         Field field = getClass().getDeclaredField("val$y");
  60.                                         field.setAccessible(true);
  61.                                         System.out.println(y);
  62.                                         field.set(Inner.this, "qqq");
  63.                 }
  64.                 catch (Exception e)
  65.                 {
  66.                         e.printStackTrace();
  67.                 }
  68. //                                System.out.println(x);本行编译报错,提示局部内部类访问必须是终态的
  69.                                 System.out.println(y);//输出y的值
  70.                         }
  71.                 }
  72.                 //奇怪么?打印3次y的结果居然是不一样的
  73.                 //外部的局部y一直是asd
  74.                
  75.                 //内部输出的y是qqq,呵呵
  76.                 Inner inner = new Inner();
  77.                 inner.inner();
  78.                 System.out.println(y);
  79.                
  80.                
  81.                 //当inner实例被返回时,x将被销毁
  82.                 return inner;
  83.         }
  84. }
复制代码

作者: 邵阳    时间: 2012-6-28 16:08
刘蕴学 发表于 2012-6-28 15:26
还得我亲自出马。。。另外就是引用类型和基本类型在这里的表现是不一样的,你区分他们的话最好拿你自己定义 ...

貌似你很厉害。所以有个问题请求。
关于值传递和引用传递。百度上什么说的都有。还有就是java中是不是只有值传递啊。
作者: 孙浩迪    时间: 2012-6-28 18:08
sunhaodi 发表于 2012-6-27 14:46
是变量的作用域的问题,因为匿名内部类是出现在一个方法的内部的,如果它要访问这个方法的参数或者方法中 ...

:L  我已经告诉说是找的嘛,怎么还警告。。。  这么深的问题,我也想明白嘛,所以找一下,正好发上,让这个同学也看下,  晕倒。。。




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2