这个匿名局部内部类为什么访问final何不使用final也可以访问外部成员?

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

假如我们有以下的代码:


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

我想这个问题应该从Java是如何实现匿名内部类的。其中有两点:
1. 匿名内部类可以使用外部类的变量(局部或成员变来那个)

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

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

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


从这两段字节码中可以看出,编译器为我们的匿名类起了一个叫MyApplication$1的名字,它包含了三个final字段(这里synthetic修饰符是指这些字段是由编译器生成的,它们并不存在于源代码中):

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

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

在匿名内部类的字节码中,存放有外部final局部变量和final参数的引用,这些引用同参数同外部final局部变量和final参数指向相同的对象。因此,在内部类中改变上述这些变量和参数,不会对外部类造成影响,因为实质上来说,内部匿名类中操作的变量,只是和上述的外部final局部变量和final参数具有相同的值(即指向了同一个对象),但变量却是不同的变量。因此,匿名类内部的改变,不会影响到外部的final局部变量和final参数。(这里的final参数指的是函数中传递的final修饰的参数)

其实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这样设计也是合理的。是啊,语法其实就一帮人创建的一种解决某些问题的方案,当然有合理和不合理之分,我们其实不用对它视若神圣。

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

、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、、

编译器是这样实现匿名内部类的,即,(1)匿名内部类持有外部类对象的引用,在匿名内部类中改变了外部类的成员,会反应到外部类上;(2)匿名内部类,实现上是通过持有外部final局部变量和final参数的备份(和外部final局部变量和final参数指向相同的对象),而不是直接操作的这些外部final局部变量和final参数,因此,在匿名类内部改变了这些变量并不会影响外部final变量和final参数,因此,外部的final局部变量和final参数的值,在匿名内部类中是不会改变的,这就是这些变量和参数需要加上final修饰的原因,使得看起来与编译器的实现更一致。

继承的出现提高了代码的复用性,并方便开发。但随之也有问题,有些类在描述完之后,不想被继承,或者有些类中的部分方法功能是固定的,不想让子类重写。可是当子类继承了这些特殊类之后,就可以对其中的方法进行重写,那怎么解决呢?
要解决上述的这些问题,需要使用到一个关键字final,final的意思为最终,不可变。final是个修饰符,它可以用来修饰类,类的成员,以及局部变量。
final修饰类不可以被继承,但是可以继承其他类。

引用类型的变量值为对象地址值,地址值不能更改,但是地址内的对象属性值可以修改。
p = p2; //final修饰的变量p,所记录的地址值不能改变
注意:声明包的语句,必须写在程序有效代码的第一行(注释不算)

当我们要使用一个类时,这个类与当前程序在同一个包中(即同一个文件夹中),或者这个类是java.lang包中的类时通常可以省略掉包名,直接使用该类。
如:cn.itcast包中有两个类,PersonTest类,与Person类。我们在PersonTest类中,访问Person类时,由于是同一个包下,访问时可以省略包名,即直接通过类名访问 Person。

我们每次使用类时,都需要写很长的包名。很麻烦,我们可以通过import导包的方式来简化。
可以通过导包的方式使用该类,可以避免使用全类名编写(即,包类.类名)。

在Java中提供了四种访问权限,使用不同的访问权限时,被修饰的内容会有不同的访问权限,以下表来说明不同权限的访问能力:

匿名对象:一个没有名字的对象
创建匿名对象直接使用,没有变量名
匿名对象在没有指定其引用变量时,只能使用一次
匿名对象可以作为方法接收的参数、方法返回值使用

    注意事项:不能以数字开头、不能是java中的关键字、区分大小写

    种类:字符串常量(用双引号括起来的内容)、整数常量(所有整数)、小数  常量(所有小数)、字符常量(用单引号括起来的内容,里面只能放单个数 字,单个字母或单个符号且必须有内容[需要代表字符])、布尔常量(true,   false)、空常量(null)、自定义常量

    进位制,一种规定的进位方法,对于任何一种进制→x进制,表示某一位置 上的数运算时逢x进一位,二进制就是逢二进一,八进制就是逢八进一等。

    正数的反码与其相同;负数反码是对其原码逐位取反,但符号位除外

    正数的补码与其相同;负数的补码是在其反码的末位加1


    在程序执行的过程中,在某个范围内其值可以发生改变的量

    定义格式:数据类型  变量名= 变量值;(存放统一类型的常量,可以重 复使用)

    JAVA语言是强类型语言,对于每一种数据都定义了明确的具体数据类型,在内存中分配了不同大小的内存空间

    作用域问题:同一区域不能使用相同的变量名

    任何数据类型用+与字符串相连接都会产生新的字符串

    ^的特点:一个数据对另一个数据位亦或两次,该数据本身不变

    流程控制语句的分类:顺序结构,选择结构,循环结构

true,就继续执行,如果是false,结束循环

获取一个数x的各个位上的值:x/10^y%10(y是位数,如果是十位就是1)

    方法:完成特定功能的代码块,提高代码的复用性

返回值类型(int):功能结果的数据类型

方法名(getSum):符合命名规则的标识符,方便调用

实际参数(调用传入的参数):实际参与运算的

形式参数(a,b):方法定义上的,用于接收实际参数

参数类型(int):参数的数据类型

参数名(a,b):变量名

返回值(sum):功能的结果,由return带给调用者

    一般来说比较推荐赋值调用,可以针对结果进一步操作

    方法不调用则不执行,方法与方法之间是平级关系,不能嵌套定义

    方法调用的时候不用再传递数据的类型,只需传递实际数据即可

方法重载概述和基本使用:

    数组的初始化:为数组开辟连续的内存空间,并未每个数组元素赋予值

格式:数据类型[] 数组名 = new 数据类型[数组的长度];

静态初始化:给出初始化值,有系统决定长度

    栈:存储局部变量(定义在方法声明上和方法中的变量)

面向过程强调过程,第一步,第二步…(例如洗衣服:第一步:放洗衣粉     浸  泡,第二步:揉搓干净,第三步:漂洗拧干,第四步:晾晒…)

强调对象,只需调用对象,具体实现由对象处理(例如洗衣服:交给全自     动  洗衣机,具体操作由洗衣机处理)

一种更符合我们思想习惯的思想,可以将复杂的事情简单化,将我们从执     行  者变了指挥官,角色发生了转换

面向对象设计:其实就是在管理和维护对象之间的关系

面向对象特征:封装,继承,多态

    类:是一组相关的属性和行为的集合 (比如:学生)

    对象:是该类事物的具体体现(比如:具体的某一个学生就是对象)

    属性:就是该事物的描述信息(事物身上的名词)

    成员变量:事物属性(和定义变量一样,只不过位置是在类中方法外)

//1,创建对象格式:类名 对象名= new 类名();

//2,使用成员变量:对象名.变量名

//3,使用成员方法:对象名.方法名(…)

成员变量:在类中方法外

局部变量:在方法定义总或者方法声明上

成员变量:在堆内存(成员变量属于对象,对象进堆内存)

局部变量:在栈内存(局部变量属于方法,方法进栈内存)

成员变量:随着对象的创建而存在,随着对象的消失而消失

局部变量:随着方法的调用而存在,随着方法的调用完毕而消失

成员变量:有默认初始化值

局部变量:没有默认初始化值,必须定义,赋值,然后才能使用

    封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式。

    构造方法概述和作用 :给对象的数据(属性)进行初始化

    重载:方法名相同,与返回值类型无关(构造方法没有返回值),只看参数列表

    静态方法只能访问静态的成员变量和和静态的成员方法

静态变量可以通过类名调用,也可以通过对象调用

成员变量只能通过对象名调用

    让类与类之间产生父子类关系,例如:动物类,猫类,狗类

    优点:提高代码的复用性以及维护性,是多态的前提

    弊端:类的耦合性增强了,开发原则:高内聚,低耦合(耦合:类与类的   关系,内聚:就是自己完成某件事情的能力)

java只支持单继承,不支持多继承,java支持多层继承(继承体系,如果想用这个体系的所有功能用最底层的类创建对象,如果想看这个体系的共性功能,看最顶层的类)

    子类只能继承父类所有非私有的成员(成员变量和成员方法)

super.成员变量 调用父类的成员变量

this.成员方法 调用本类的成员方法,也可以调用父类的方法

super.成员方法 调用父类的成员方法

子类中所有的构造方法默认都会访问父类中空参数的构造方法,因为子类会继承父类中的数据,可能还会使用父类的数据,所以子类初始化之前,一定要先完成父类数据的初始化,其实每一个构造方法的第一条语句默认都是:super() Object类最顶层的父类

    重写:子父类出现了一模一样的方法(返回值类型可以是子父类)

容,可以重写父类中的方法。这样,即沿袭了父类的功能,又定义了    子类特有的内容

    父类中私有方法不能被重写,因为父类私有方法子类根本无法继承

    子类重写父类方法时,访问权限不能更低,最好一致

    父类静态方法,子类也必须通过静态方法进行重写(静态只能覆盖静态)

    子类重写父类方法的时候,最好声明一模一样

方法重载:本类中出现的方法名一样,参数列表不同的方法。与返回值类     型无关。可以改变返回值类型,只看参数列表

    子类对象调用方法的时候:先找子类本身,再找父类。

    引用类型:地址不能被改变,对象中的属性可以改变

    在对象构造完毕前即可,比如在构造方法中赋值

    多态前提:要有继承关系,要有方法重写,要有父类引用指向子类对象

    成员变量:编译看左边(父类),运行看左边(父类)

    成员方法:编译看左边(父类),运行看右边(子类),动态绑定

    静态方法:编译看左边(父类),运行看左边(父类)

    只有非静态的成员方法,编译看左边,运行看右边

    构造方法:有,用于子类访问父类数据的初始化

    成员方法:既可以是抽象的,也可以是非抽象的

    抽象方法,强制要求子类做的事情(必须重写)

    非抽象方法,子类继承的事情,提高代码复用性(子类继承可直接使用)

如果一个抽象类没有抽象方法存在的意义:不让其他类创建本类对象,交     给子类完成

类,接口相互之间的关系:

成员变量:可以是变量,也可以是常量

成员方法:可以抽象,也可以非抽象

成员变量:只可以是常量

成员方法:只可以是抽象方法

类与接口:实现,单实现,多实现

接口与接口:继承,单继承,多继承

抽象类:被继承体现的是:“is a”的关系,抽象类中定义的是该继     承体系的共性功能

接口:被实现体现的是:“like a”的关系,接口中定义的是该继承     体系的扩展功能

    概述:存文件的文件夹(将文件按照功能或者模块归类)

    作用:将字节码进行分类存放,包其实就是文件夹

权限修饰符:默认修饰符,public

状态修饰符:final

用的最多的就是:private

用的最多的就是:public

用的最多的就是:public

在外部类中创建方法,访问本类中私有的内部类即可

外部类名.内部类名 对象名 = 外部类名.内部类对象;

局部内部类访问局部变量的问题:

(局部内部类:方法中定义的内部类,只能在其方法中访问)

匿名内部类的格式和理解:

    本质:是一个继承了该类或者实现了该接口的子类匿名对象

    匿名内部类是不能向上转型的,因为没有子类类名

    匿名内部类当做参数传递(本质:把匿名内部类看做一个对象)


我要回帖

更多关于 局部内部类为什么访问final 的文章

 

随机推荐