Java
语言基础

为什么传入匿名内部类的参数需要用final修饰

简介:从程序设计语言的理论上,局部内部类(即定义在方法中的内部类)由于本身就是在方法内部(可出现在形式参数定义处或者方法体处),因而访问方法中的局部变量(形式参数或局部变量)是天经地义的,是很自然的。

从程序设计语言的理论上,局部内部类(即定义在方法中的内部类)由于本身就是在方法内部(可出现在形式参数定义处或者方法体处),因而访问方法中的局部变量(形式参数或局部变量)是天经地义的,是很自然的。

为什么Java中要加上一条限制只能访问final型的局部变量?Java语言的编译程序的设计者当然全实现局部内部类能访问方法中的所有的局部变量(因为从理论上这是很自然的要求),但是编译技术是无法实现的或代价极高。这是因为,局部变量的生命周期与局部内部类的对象的生命周期的不一致性。

假设方法f()被调用,从而在它的调用栈中生成了变量i,此时产生了一个局部内部类对象inner_object,它访问了该局部变量i。当方法f()运行结束后,局部变量i就已死亡了,不存在了。但局部内部类对象inner_object还可能一直存在(只有没有任何引用指向该对象时,它才会被垃圾回收器收集),它不会随着方法f()运行结束死亡。这时出现了一个“荒唐”结果:局部内部类对象inner_object要访问一个已不存在的局部变量i

如何才能实现?当变量i是使用final修饰时,将会把局部变量“复制”一份,复制品直接作为局部内部中的数据成员。这样当局部内部类访问局部变量时,其实真正访问的是这个局部变量的“复制品”(即这个复制品就代表了那个局部变量)。因此当运行栈中的真正的局部变量死亡时,局部内部类对象仍可以访问局部变量(其实访问的是“复制品”),给人的感觉好像是局部变量的“生命期”延长了。

那么核心的问题是怎么才能使得访问“复制品”与访问真正的原始的局部变量,其语义效果是一样的呢?当变量是final修饰时,若是基本数据类型,由于其值不变,因而其复制品与原始的量是一样,永远指向同一个对象(由于是final,从而保证只能指向这个对象,再不能指向其它对象),语义效果相同(若不是final,就无法保证复制品与原始变量保持一致了,因为在方法中改的是原始变量,而局部内部类中改的是复制品),达到局部内部类中访问的复制品与方法代码中访问的原始对象。否则当方法中修改原始变量,而局部内部类中修改复制品时,就无法保证复制品与原始变量保持一致了(因此它们原本就应该是同一个变量)。

现在我们来看,如果我要实现一个在一个方法中匿名调用InnerClass的例子:

  • public static void test(final String s) {
  • // 或final String s = "admin";
  • InnerClass c = new InnerClass() {
  • public void m() {
  • int x = s.hashCode();
  • System.out.println(x);
  • }
  • };
  • // 其它代码
  • }

从代码上看,在一个方法内部定义的内部类的方法访问外部方法内局部变量或方法参数,是非常自然的事,但内部类编译的时候如何获取这个变量,因为内部类除了它的生命周期是在方法内部,其它的方面它就是一个普通类。那么它外面的那个局部变量或方法参数怎么被内部类访问?编译器在实现时实际上是这样的:

  • public static void test(final String s) {
  • // 或final String s = "admin";
  • class OuterClass$1 extends InnerClass {
  • private final String s;
  • public OuterClass$1(String s) {
  • this.s = s;
  • }
  • public void m() {
  • int x = s.hashCode();
  • System.out.println(x);
  • }
  • };
  • InnerClass c = new OuterClass$1(s);
  • // 其它代码
  • }

即外部类的变量被作为构造方法的参数传给了内部类的私有成员。假如没有final,那么下面的代码:

  • public static void test(String s) {
  • // 或String s = "admin";
  • InnerClass c = new InnerClass() {
  • public void m() {
  • s = "other";
  • }
  • };
  • System.out.println(s);
  • }

就会编译成如下代码:

  • public static void test(String s) {
  • // 或String s = "admin";
  • class OuterClass$1 extends InnerClass {
  • private String s;
  • public OuterClass$1(String s) {
  • this.s = s;
  • }
  • public void m() {
  • s = "other";
  • }
  • };
  • InnerClass c = new OuterClass$1(s);
  • }

内部类的s重新指向other并不影响test(String)方法的参数或外部定义的那个s,同理如果外部的s重新赋值内部类的s也不会跟着改变。而我们看到的下面代码:

  • public static void test(String s) {
  • //或String s = "admin";
  • InnerClass c = new InnerClass() {
  • public void m() {
  • s = "other";
  • }
  • };
  • System.out.println(s);
  • }

在语法上是一个s,在内部类中被改变了,但结果打印的出来的你认为是同一的s却还是原来的admin。所以final从语法上约束了实际上两个不同变量的一致性(表现为同一变量)。