【转发】Java匿名类中使用的局部变量为何要加final

转发自CSDN,作者未标明原作者。http://blog.csdn.net/rookieding/article/details/38707951

原作者思路很清晰。Thx!


 这几天,在网上找了一些关于final的知识,当然并不全面,有的一时也没有很好的理解,先收集起来,理理思路,把不懂的画出来,以便更好地学习……
java中的final关键字通常的指的是“这是无法改变的”。它可能被做为三种的修饰词.------数据(基本类型,对象或者数组),方法(类方法、实例方法),类。
<1>final应用于类
如果类被声明为final,则表示类不能被继承,也就是说不能有子类。因为不能有子类,所以final类不能被声明为abstract抽象类。所以final关键字和abstract关键字不能同时使用。
一个final类中的所有方法都隐式地指定为final
<2>final应用于类方法
使用final使用的原因有两个:一. 把方法锁定,使得在子类(导出类)中不能修改它的含义。二.效率,一个final方法被调用时会转为内嵌调用,不会使用常规的压栈方式,使得运行效率较高,尤其是在方法体较简单的 情 况下,但也并不绝对。(与C++中的inline关键字类似)
类的方法分为“类方法”(类中static方法)和“实例方法”(类中非static方法)。不管是类方法还是实例方法,只要被声明为final,就表示不能被子类“覆盖”或“隐藏”,也就是不能在子类中声明为“一模一样”的方法。
特别的:类中的所有private方法都隐式地指定为是final,所以在继承关系中不存在覆盖问题。
<3>final应用于类属性
类中的属性,也分为“类属性”(静态属性)和“实例属性”(非静态属性)。不管是静态属性还是非静态属性,只要被声明为final,则属性的值只能被指定一次(也就是说初始化后就不能再给属性赋值),而且必须进行“显示初始化”。
对于类的静态属性,可以完成显示初始化的地方有两个:一个是在声明静态属性的时候就进行初始化;另一个,也可以在“静态初始化块”中进行初始化。
对于类的非静态属性,可以完成显示初始化的地方有三个:一个是在声明属性的时候就进行初始化;另一个,也可以在“初始化块”中进行初始化;还可以在构造方法中进行初始化。
需要注意的是,在“静态初始化块”中不能访问“实例属性”,那当然就不能完成“实例属性”的“显示初始化”任务了;在“初始化块”中能访问“类属性”,但是不能完成“类属性”的“显示初始化”任务,类属性的显示初始化应该在“静态初始化块”中。
如果final属性在声明的时候没有进行初始化,我们管这种final叫做“空白final”。但必须确保空白final在使用前被初始化,一般在构造方法完成。
注:在类中,如果类的属性(包括类属性和实例属性)没有被final所修饰,那么即使我们没有进行“显示初始化”,那么编译器也会给他们进行默认初始化。而对于final属性我们必须进行“显示初始化”。
<4>final应用于方法参数或变量
final也可以修饰方法参数或变量。
final变量可以在声明的时候不进行“显示初始化”,(而且只要不对它进行使用,就不会报编译错误,但是既然不使用,这个变量也就没什么用,显然是多余的),只要在使用之前进行初始化就行了,这个特性和普通的变量是一样的。
方法参数被声明为final,表示它是只读的。
注意:方法中的变量可以声明为final,但是不能声明为static,不管这个方法是静态的还是非静态的。
在谈final之前我们现看一个最简单的程序:

Class Main{
    public String a=Test.aa;
    public String b=Test.bb;
    public static void main(String args[]){
    }
}
Class Test{
    public final static String aa="HelloA";
    public final static String bb=new String("HelloB");
}

大家肯定要问了,Main中的a 和 b 到底有什么区别?
我们先什么都不说,看一下反编译的结果。

zheng@zheng-laptop:~/workspace/Test/src$ javap -c Main 
Compiled from "Main.java"
public class Main extends java.lang.Object{
    public java.lang.String MainA;
    public java.lang.String MainB;
    public Main();
    Code:
    0: aload_0
    1: invokespecial #1; //Method java/lang/Object."<init>":()V
    4: aload_0
    5: ldc #2; //String HelloA
    7: putfield #3; //Field MainA:Ljava/lang/String;
    10: aload_0
    11: getstatic #4; //Field Test.bb:Ljava/lang/String;
    14: putfield #5; //Field MainB:Ljava/lang/String;
    17: return
    public static void main(java.lang.String[]);
    Code:
    0:return
}

在第5行java进行了一次值赋值,所以就直接霸HelloA给Main.a了。 而在第11行,java则是传给b一个Object,也就是将Test.bb给了Main.b;
这是为什么了,C++中说 const其中一个功能就是为了加快编译速度,的确值赋值加快了编译的速度(java应该也是这么做的)
final放在什么内存中等把JVM搞懂了,再来补上!
但是这可能会引发一个问题,如果修改aa的值,并且就编译Test.java,Main中的a还是原来的”HelloA“,没有改变,因为final在这种情况下是直接赋值的。
对与java中的final变量,java编译器是进行了优化的。每个使用了final类型变量的地方都不会通过连接而进行访问。比如说Test类中使用了Data类中一个final的int数字fNumber=77,这时候,java编译器会将77这个常数编译到Test类的指令码或者常量池中。这样,每次Test类用到fNumber的时候,不会通过引用连接到Data类中进行读取,而是直接使用自己保存在类文件中的副本。
用程序说话:

Test.java:
public class Test{
    public static void main(String[] args){
        System.out.println(Data.fNumber);
        }
    }
Data.java:
public class Data{
    public static final int fNumber=77; 
}

执行命令和结果:

Microsoft Windows XP [版本 5.1.2600](C) 版权所有 1985-2001 Microsoft Corp.
C:\Documents and Settings\zangmeng>cd ..
C:\Documents and Settings>cd ..
C:\>javac Test.java
C:\>java Test77
C:\>

这时候,我们更改Data.java的内容:

public class Data{
    public static final int fNumber=777; 
}

然后执行如下命令:

C:\>javac Data.java
C:\>java Test77
C:\>

这里我们看到,虽然Data.java中的fNumber已经更改为777,而且已经重新编译了,但是因为编译器把fNumber的副本保存Test类中,所以在重新编译Test类的前,Test类一直把fNumber认为是77而不是777。
下面我们变异Test.java,再执行,看看结果。

C:\>javac Test.java
C:\>java Test777
C:\>

这时候,我们看到,重新编译的Test类将新的777数值封装到了自己类中。
整个过程如下:

Microsoft Windows XP [版本 5.1.2600](C) 版权所有 1985-2001 Microsoft Corp.
C:\Documents and Settings\zangmeng>cd ..
C:\Documents and Settings>cd ..
C:\>javac Test.java
C:\>java Test77//在这里改变了Data.java的内容C:\>javac Data.java
C:\>java Test77
C:\>javac Test.java
C:\>java Test777
C:\>

这个是java编译器的优化,具体的,大家可以继续参考http://www.blogjava.net/aoxj/archive/2009/11/10/165536.html
另外java的final还有inline的功能,这个和C++有异曲同工之妙。简单的来说就是内联函数就是指函数在被调用的地方直接展开,编译器在调用时不用像一般函数那样,参数压栈,返回时参数出栈以及资源释放等,这样提高了程序执行速度。
但 是网上有人说这样并没有加快速度,这是为什么呢?还不太清楚!!

final使得被修饰的变量"不变",但是由于对象型变量的本质是“引用”,使得“不变”也有了两种含义:引用本身的不变,和引用指向的对象不变。
引用本身的不变:

final StringBuffer a=new StringBuffer("immutable");
final StringBuffer b=new StringBuffer("not immutable");
a=b;//编译期错误

引用指向的对象不变:

final StringBuffer a=new StringBuffer("immutable");
a.append(" broken!"); //编译通过

可见,final只对引用的“值”(也即它所指向的那个对象的内存地址)有效,它迫使引用只能指向初始指向的那个对象,改变它的指向会导致编译期错误。至于它所指向的对象的变化,final是不负责的。这很类似==操作符:==操作符只负责引用的“值”相等,至于这个地址所指向的对象内容是否相等,==操作符是不管的。
Java的局部内部类以及final类型的参数和变量
本文是Thinking In Java中其中一段的阅读总结。如果定义一个匿名内部类,并且希望它使用一个在其外部定的对象,那么编译器会要求其参数引用是final 的。经研究,Java虚拟机的实现方式是,编译器会探测局部内部类中是否有直接使用外部定义变量的情况,如果有访问就会定义一个同类型的变量,然后在构造方法中用外部变量给自己定义的变量赋值。
Thinking In Java里面的说法(唯一正确的说法): 如果定义一个匿名内部类,并且希望它使用一个在其外部定的对象,那么编译器会要求其参数引用是final 的。

public class Tester {
    public static void main(String[] args) {
        A a = new A();
        C c = new C();
        c.shoutc(a.shout(5));
    } 
} ////////////////////////////////////////////////////////
class A {
    public void shouta() {
        System.out.println("Hello A");
    }
    public A shout(final int arg) {
        class B extends A {
            public void shouta() {
                System.out.println("Hello B" + arg);
            }
        }
        return new B();
    } 
} ////////////////////////////////////////////////////////
class C {
    void shoutc(A a) {
        a.shouta();
    } 
} 

c.shoutc(a.shout(5)),在a.shout(5)得到返回值后,a的shout()方法栈被清空了,即arg不存在了,而c.shoutc()却又调用了a.shouta()去执行System.out.println("Hello B" + arg)。
再来看Java虚拟机是怎么实现这个诡异的访问的:有人认为这种访问之所以能完成,是因为arg是final的,由于变量的生命周期,事实是这样的吗?方法栈都不存在了,变量即使存在,怎么可能还被访问到?试想下:一个方法能访问另一个方法的定义的final局部变量吗(不通过返回值)?
研究一下这个诡异的访问执行的原理,用反射探测一下局部内部类 。编译器会探测局部内部类中是否有直接使用外部定义变量的情况,如果有访问就会定义一个同类型的变量,然后在构造方法中用外部变量给自己定义的变量赋值,而后局部内部类所使用的变量都是自己定义的变量,所以就可以访问了。见下:

class A$1$B {
    A$1$B(A, int);
    private final int var$arg;
    private final A this$0;
} 

A$1$B类型的对象会使用自定义的var$arg变量,而不是shout()方法中的final intarg变量,当然就可以访问了。
那么为什么外部变量要是final的呢?即使外部变量不是final,编译器也可以如此处理:自己定义一个同类型的变量,然后在构造方法中赋值就行了。原因就是为了让我们能够挺合逻辑的直接使用外部变量,而且看起来是在始终使用 外部的arg变量(而不是赋值以后的自己的字段)。
考虑出现这种情况:在局部内部类中使用外部变量arg,如果编译器允许arg不是final的,那么就可以对这个变量作变值操作(例如arg++),根据前面的分析,变值操作改变的是var$arg,而外部的变量arg并没有变,仍然是5(var$arg才是6)。因此为了避免这样如此不合逻辑的事情发生:你用了外部变量,又改变了变量的值,但那个变量却没有变化,自然的arg就被强行规定必须是final所修饰的,以确保让两个值永远一样,或所指向的对象永远一样(后者可能更重要)。
还有一点需要注意的是内部类与方法不是同时执行的,比如实现ActionListener,只有当事件发生的时候才会执行,而这时方法已经结束了。
也有人这样说:匿名内部类要访问局部变量,但是函数的局部变量在执行完后会立即退出,销毁掉所有临时变量。而产生的匿名内部类可能会保留。在java中方法不是对象,不存储状态,这时候匿名内部类已经没有外部环境了。我猜想匿名内部类可能会把需要访问的外部变量作为一个隐藏的字段,这样只是得到了一个变量的引用拷贝,所以是只读的,所以编译器要求给要访问的外部局部变量加final。
可以用一个包装对象来突破这一限制。

final Result result=new Result();
sqlMaker.selectById(id).execute(getTransaction(),new Function(){
        public Object call(Object... args) {
            ResultSet rs=(ResultSet)args[0];
            Object obj=sqlMaker.getTable().readFirstObject(rs);
            result.setValue(obj);
            return null;
        }
    }
);
T r= (T)result.getValue()

理解final问题有很重要的含义。许多程序漏洞都基于此----final只能保证引用永远指向固定对象,不能保证那个对象的状态不变。在多线程的操作中,一个对象会被多个线程共享或修改,一个线程对对象无意识的修改可能会导致另一个使用此对象的线程崩溃。一个错误的解决方法就是在此对象新建的时候把它声明为final,意图使得它“永远不变”。其实那是徒劳的
请教大家个问题,想了好久也不明白,为什么在某方法内定义一个匿名内部类,并且希望它使用外部定义的对象,那么要求此方法的参数引用要声明为final?(我只知道final的作用对于对象引用来説,此对象引用不能指向新的对象,对于基本类型就是不能改变它的值)
因为内部要copy一份自己使用,怕你在外边改了造成一些不确定的问题。所以干脆final
http://forums.sun.com/thread.jspa?threadID=5325241&messageID=10392871
这是一个编译器设计的问题,如果你了解java的编译原理的话很容易理解。 首先,内部类被编译的时候会生成一个单独的内部类的.class文件,这个文件并不与外部类在同一class文件中。 当外部类传的参数被内部类调用时,从java程序的角度来看是直接的调用
例如:

public void dosome(final String a,final int b){ 
    class Dosome{
        public void dosome(){
            System.out.println(a+b);
        }
    }; 
    Dosome some=new Dosome(); 
    some.dosome(); 
} 

从代码来看好像是那个内部类直接调用的a参数和b参数,但是实际上不是,在java编译器编译以后实际的操作代码是

class Outer$Dosome{ 
    public Dosome(final String a,final int b){ 
        this.Dosome$a=a; 
        this.Dosome$b=b; 
    } 
    public void dosome(){ 
        System.out.println(this.Dosome$a+this.Dosome$b); 
    } 
}

从以上代码看来,内部类并不是直接调用方法传进来的参数,而是内部类将传进来的参数通过自己的构造器备份到了自己的内部,自己内部的方法调用的实际是自己的属性而不是外部类方法的参数。 这样理解就很容易得出为什么要用final了,因为两者从外表看起来是同一个东西,实际上却不是这样,如果内部类改掉了这些参数的值也不可能影响到原参数,然而这样却失去了参数的一致性,因为从编程人员的角度来看他们是同一个东西,如果编程人员在程序设计的时候在内部类中改掉参数的值,但是外部调用的时候又发现值其实没有被改掉,这就让人非常的难以理解和接受,为了避免这种尴尬的问题存在,所以编译器设计人员把内部类能够使用的参数设定为必须是final来规避这种莫名其妙错误的存在。
实现的确是如此,不过final只是让一个引用不能修改而已,照样可以修改它指向的数据的内容。
再 一次阐述 内部类,final
1)所谓“局部内部类”就是在对象的方法成员内部定义的类。而方法中的类,访问同一个方法中的局部变量,是天经地义的。那么为什么要加上一个final呢?

2)原因是:编译程序实现上的困难,难在何处:内部类对象的生命周期会超过局部变量的生命期。为什么?表现在:局部变量的生命期:当该方法被调用时,该方法中的局部变量在栈中被创建(诞生),当方法调用结束时(执行完毕),退栈,这些局部变量全部死亡。而:内部类对象生命期,与其它类一样,当创建一个该局部类对象后,只有没有其它人再引用它时,它才能死亡。完全可能:一个方法已调用结束(局部变量已死亡),但该局部类的对象仍然活着。即:局部类的对象生命期会超过局部变量。

3)退一万步:局部类的对象生命期会超过局部变量又怎样?问题的真正核心是:如果:局部内部类的对象访问同一个方法中的局部变量,是天经地义的,那么:只要局部内部类对象还活着,则:栈中的那些它要访问的局部变量就不能“死亡”(否则:它都死了,还访问个什么呢?),这就是说:局部变量的生命期至少等于或大于局部内部类对象的生命期。而:正是这一点是不可能做到的

4)但是从理论上:局部内部类的对象访问同一个方法中的局部变量,是天经地义的。所以:经过努力,达到一个折中结果:即:局部内部类的对象可以访问同一个方法中的局部变量,只要这个变量被定义为final.那么:为什么定义为final变可以呢?定义为final后,编译程序就好实现了:具体实现方法是:将所有的局部内部类对象要访问的final型局部变量,都成员该内部类对象中的一个数据成员。这样,即使栈中局部变量(含final)已死亡,但由于它是final,其值永不变,因而局部内部类对象在变量死亡后,照样可以访问final型局部变量。


不管变量是不是final,他的生命周期都在于{}中。
不管对象是不是final,他的生命周期都是 new开始,垃圾回收结束。
类对象(class对象)与其它对象不同,类对象的生命周期 开始于类被加到内存中那一刻,结束于垃圾回收。 类变量(static)与类对象的生命周期相同。
解析就是对于编译型常量使用直接的内存地址代替变量,如final static int a =10;但是对于在编译的时候不能得到具体值得变量不做变换,如final static inta = Math.random()。
final和abstract一样,都是非访问控制符,当然也不会改变作用域protect,private,public才是访问控制符。

 

posted on 2015-04-23 14:06  Erbin  阅读(363)  评论(0编辑  收藏  举报

导航