一、内部类
1️⃣作为其外部类成员的内部类,称为成员内部类。除另有说明外,“内部类”通常是指成员内部类。
2️⃣局部类和匿名类是内部类的两个特殊的类型:
- 在方法体中声明的内部类,称为局部内部类,亦称局部类。局部类是有类名的。
- 在方法体中声明的无需命名的内部类,称为匿名内部类,亦称匿名类。匿名类是没有类名的。
二、局部变量
局部变量,也称内部变量,是指在一个函数内部或复合语句内部定义的变量。局部变量的作用域是定义该变量的函数或定义该变量的复合语句。局部变量的生存期是从函数被调用的时刻算起到函数返回调用处的时刻结束。
因此,内部类能够访问某局部变量,说明这个内部类不是在类中定义的内部类,而是在方法中定义的内部类,称之为:局部内部类没有类名的局部内部类叫匿名内部类
。
局部变量的作用域:局部变量是在某个方法中定义,当该方法执行完成后,局部变量也就消失了。【局部变量分配在JVM的虚拟机栈中,这部分内存空间随着程序的执行自动回收】,也即:局部变量的作用域是在 “方法的范围内”。但是,当(局部)内部类访问局部变量时,会扩大局部变量的作用域。
三、示例
public class finalLocalVariable {
public static void main(String[] args) {
final String str = "HelloWorld!";//局部变量
//局部内部类
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 1000; i++)
System.out.println(str);
}
}).start();
System.out.println("main thread finished");
}
}
第 3 行 main() 中定义了一个局部变量 str,第 5 行定义了一个局部内部类 Thread,并且在局部内部类 Thread 中访问 str。按理说:当程序执行到第 17 行时,main() 就结束了,也即:主线程结束了。局部变量 str 的生命周期也应该结束了。但是,Thread 线程还未结束,在 Thread 线程中还能够打印局部 str 的值。这表明:局部变量 str 作用域被扩大了。
因此,如果局部变量不用final修饰,就可以在(局部)内部类中随意修改该局部变量的值。同时在该局部变量的作用域范围之外可以看到这些修改后的值,如此会发生莫名的问题。因此,Java 就规定(局部)内部类访问的局部变量必须用 final 修饰,以防止更改局部变量的值。
分析:
-
生命周期不同:局部变量直接存储在栈中,当方法执行结束,非 final 的局部变量就被销毁,而局部内部类对局部变量的引用依然存在,当局部内部类要调用局部变量时,就会出错,出现非法引用。简单来说,就是非 final 的局部变量的生命周期比局部内部类的生命周期短,是不是直接可以拷贝变量到局部内部类?这样内部类中就可以使用而且不担心生命周期问题呢?也是不可以的,因为直接拷贝又会出现第二个问题,就是数据不同步。
-
数据不同步:内部类并不是直接使用传递进来的参数,而是将传递进来的参数通过自己的构造器备份到自己内部,表面看是同一个变量,实际调用的是自己的属性而不是外部类方法的参数,如果在内部类中,修改了这些参数,并不会对外部变量产生影响,仅仅改变局部内部类中备份的参数。但是在外部调用时发现值并没有被修改,如此就会造成数据不同步。所以使用 final 避免数据不同步的问题。
为什么添加 final 修饰的局部变量,就可以被局部内部类引用呢?
若定义为 final,Java 编译器就会在内部类生成一个外部变量的拷贝,既保证内部类引用外部属性,又能保证值的唯一性。也就是拷贝了一个变量的副本,提供给局部内部类,该副本的生命周期和局部内部类一样长,并且不可修改,保证了数据的同步。
四、原因
-
因为内部类被编译的时候会生成一个单独的内部类的.class文件,这个文件并不与外部类在同一class文件中。如果内部类改掉了这些参数的值也不可能影响到原参数,然而这样却失去了参数的一致性。因为从开发者的角度来看它们是同一个东西,如果开发者在程序设计的时候在内部类中改掉参数的值,但是外部调用的时候又发现值其实没有被改掉,这就让人非常的难以理解和接受,为了避免这种问题发生,所以编译器把内部类能够使用的参数设定为必须是 final 来规避这种莫名其妙错误的存在。
-
当方法被调用运行完毕之后,局部变量就已消亡了。但内部类对象可能还存在,直到没有被引用时才会消亡。此时就会出现一种情况,就是内部类要访问一个不存在的局部变量。
-
解决这一问题的办法就是使用 final 修饰局部变量,通过将 final 局部变量“复制”一份,复制品直接作为方法内部类中的数据成员,此时方法内部类访问的其实是这个局部变量的复制品!而且,由于被 final 修饰的变量赋值后不能再修改,所以就保证了复制品与原始变量的一致。
-
原因二的功能能实现的原因是:Java 采用了一种 copy local variable(复制局部变量)的方式来实现,也就是说把定义为 final 的局部变量拷贝过来用,而引用的也可以拿过来用,只是不能重新赋值。从而造成了可以 access local variable(访问局部变量)的假象,而这个时候由于不能重新赋值,所以一般不会造成不可预料的事情发生。
-
使用 final 修饰符不仅会保持对象的引用不会改变,而且编译器还会持续维护这个对象在回调方法中的生命周期。这是 final 变量和 final 参数的根本意义。