一、前言
在研究公司某个项目的源码,看到前人使用了挺多内部类,内部类平时我用的比较多的是匿名内部类,平时用的多的是匿名内部类,其他形式的用的比较少,然后我就有个疑惑:到底内部类是基于什么样的考虑,才让java设计者搞这么一套实现?还有,平时在什么情境下会考虑使用内部类呢?这个我将在另外一篇博文进行介绍,详情参见:xxxxxx,本篇博文是在查阅内部类相关资料时,看到很多博文在解释为什么匿名内部类在引用局部变量时需要将局部变量指定为final(不可变)说的不清楚,所以这边我将我的理解写出来,以供参考~
二、正文
本篇博文基于的Java环境是:jdk1.6,在jdk1.8中,匿名内部类引用局部变量时,已经废弃使用final修饰局部变量,但这并不意味着jdk1.8在实现方面有什么很大的变动,这个博文最后也会给予说明。
在讨论本篇博文之前,需要与看官达成一些共识:
1)Java中数据类型分为两大类:基本数据类型和引用数据类型。相应的,变量也分为基本类型和引用类型。基本类型的变量保存原始值,即它代表的值就是数值本身,而引用类型的变量保存引用值,"引用值"指向内存空间的地址(也就是地址值),代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置
2)final修饰的变量,赋予过初值之后,不允许变更。对基本类型而言,其保存的原始值不能变更,对引用类型而言,其引用的地址值不允许变更,也就是说不能引用其他对象
final int a = 1; a = 2;//编译报错 final Integer b = new Integer(1); b = new Integer(2);//编译报错
先来个匿名内部类使用局部变量的例子:
public class OutClass { private String msg = "OutClass's msg"; public void check(final String m){ (new Thread(){ public void run(){ System.out.println(m); System.out.println(msg); } }).start(); } public static void main(String[]args){ OutClass oc = new OutClass(); oc.check("Need to pass to inner Class"); } } //输出 Need to pass to inner Class OutClass's msg
编译器编译的时候,匿名内部类会生成一个:外部类$x.class,其中x是正整数,这个实例中匿名内部类为:OutClass$1.class,我们可以使用jdk自带的反编译工具,将生成的这个class反编译出来看下,在dos窗口执行:javap -c OutClass$1.class,
我相信,只要熟悉内部类的童鞋,都知道,如果要实例化内部类,那么必须先实例化外部类,这就是原因所在,内部类需要持有外部类的一个引用,这也是内部类能无条件访问外部类所有成员方法和成员变量的根本原因。
通过上面反编译出来的内容可以看出,如果匿名内部类使用了局部变量,那么编译器会将使用的值拷贝一份,作为构造函数的一个参数传递进来(构造函数是编译器自动添加)。因为局部变量在方法或者代码块执行完毕,就会被销毁,所以编译器在编译的时候,就拷贝了一份局部变量存储的字面值或者地址值,这样局部变量被销毁时,匿名内部类依然拥有之前传递进来的值。现在我们从语义上来理解下Java设计者的考虑:假如传递到匿名内部类的局部变量,不加final修饰,那么意味着局部变量可以改变,这就意味着匿名内部类里面值的变更和外部的变量的变更不能同步,虽然内部类持有的是局部变量值的拷贝,但是语义应该保持一致,语义保持一致的前提是值要能同步,因为java编译器的设计无法提供两边同步变更的机制,所以直接锁死,不允许内外变更~
1 package com.finalparams.main; 2 3 public class Main { 4 public static void main(String[]args){ 5 Main m = new Main(); 6 m.show(); 7 } 8 9 public void show(){ 10 int a = 1; 11 (new innerClassInterface(){ 12 public void doChange(){ 13 a = 3;//编译报错:Cannot refer to the non-final local variable a defined in an enclosing scope 14 } 15 }).doChange(); 16 System.out.println(a); 17 } 18 } 19 20 interface innerClassInterface{ 21 public void doChange(); 22 }
以上代码第13行编译报错,假设编译器允许编译过去,按照编译器现有的实现,内部类里面的变更是没有办法反应到外面的, 也就是里面变更了,但是外部还是没变,这样是不能接受的,所以直接锁死,不让变更~
特别说明:在jdk1.8之后,匿名内部类使用局部变量的时候,局部变量已经不需要使用final修饰了,可以编译通过,那是不是说编译器的实现机制变了呢?非也,只是编译器在编译的时候不允许对这个变量进行变更,也就是说,如果你内外只是使用这个变量,而不进行重新赋值,那么就编译通过,如果内外有重新赋值,那么还是会报编译错误:
...................................................................................... public void show(){ int a = 1; (new innerClassInterface(){ public void doChange(){ System.out.println(a);//jdk1.8之前编译报错,1.8之后,不报错,因为没有改变变量的值 } }).doChange(); System.out.println(a); } ......................................................................................
public void show(){ int a = 1; (new innerClassInterface(){ public void doChange(){ System.out.println(a);//jdk1.8之前编译报错,1.8之后,不报错,因为没有改变变量的值 a = 4;//jdk1.8编译报错 } }).doChange(); System.out.println(a); }
三、参考链接
https://www.zhihu.com/question/21395848
https://zhidao.baidu.com/question/266984801594415645.html
四、联系本人
为方便没有博客园账号的读者交流,特意建立一个企鹅群(纯公益,非利益相关),读者如果有对博文不明之处,欢迎加群交流:261746360,小杜比亚-博客园