java匿名内部类使用外部变量时,外部变量必须是final,为什么?

时间:2022-01-22 18:24:32

1,Java中的匿名内部类是如何实现的?

先定义一个接口:

1
2
3
public interface MyInterface {
void doSomething();
}
然后创建这个接口的匿名子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TryUsingAnonymousClass {
public void useMyInterface() {
final Integer number = 123;
System.out.println(number);

MyInterface myInterface = new MyInterface() {
@Override
public void doSomething() {
System.out.println(number);
}
};
myInterface.doSomething();

System.out.println(number);
}
}
这个匿名子类会被编译成一个单独的类,反编译的结果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TryUsingAnonymousClass$1
implements MyInterface {
private final TryUsingAnonymousClass this$0;
private final Integer paramInteger;

TryUsingAnonymousClass$1(TryUsingAnonymousClass this$0, Integer paramInteger) {
this.this$0 = this$0;
this.paramInteger = paramInteger;
}

public void doSomething() {
System.out.println(this.paramInteger);
}
}
可以看到 外部类 和名为number的 局部变量 是作为 构造方法的参数 传入匿名内部类的


2,为什么局部变量要作为内部类构造方法的参数传入?

为了解决:局部变量的生命周期与局部内部类的对象的生命周期的不一致性问题

(1)问题:设方法useMyInterface被调用,从而在它的调用栈中生成了变量number,此时产生了一个局部内部类对象myInterface,它访问了该局部变量number .

当方法useMyInterface运行结束后,局部变量number就已死亡了,不存在了.

但局部内部类对象myInterface还可能一直存在(只能没有人再引用该对象时,它才会死亡),它不会随着方法useMyInterface运行结束死亡.这时出现了一个"荒唐"结果:局部内部类对象myInterface要访问一个已不存在的局部变量number



(2)解决方法:通过将局部变量"复制"一份,复制品直接作为局部内部类的数据成员.这样:当局部内部类访问局部变量 时,其实真正访问的是这个局部变量的"复制品"(即:这个复制品就代表了那个局部变量).因此:当运行栈中的真正的局部变量死亡时,局部内部类对象仍可以 访问局部变量(其实访问的是"复制品"),给人的感觉:好像是局部变量的"生命期"延长了.


(3)为什么会与final有关系?

为了保证局部变量和 内部类中复制品 的数据一致性。


若是基本数据类型,当变量是final时,由于其值不变,因而:其复制品与原始的量是一样.语义效果相同

若:不是final,就无法保证:复制品与原始变量保持一致了,因为:在方法中改的是原始变量,而局部内部类中改的是复制品


若是引用类型,当 变量是final时,由于其引用值不变(即:永远指向同一个对象),因而:其复制品与原始的引用变量一样,永远指向同一个对象(由于是 final,从而保证:只能指向这个对象,再不能指向其它对象),达到:局部内部类中访问的复制品与方法代码中访问的原始对象,永远都是同一个即:语义效 果是一样的

.否则:当方法中改原始变量,而局部内部类中改复制品时,就无法保证:复制品与原始变量保持一致了(因此:它们原本就应该是同一个变量.) 


3,例子

现在我们来看,如果我要实现一个在一个方法中匿名调用ABSClass的例子: 
 public static void test(final String s){ 
     //或final String s = "axman"; 
  ABSClass c = new ABSClass(){ 
   public void m(){ 
      int x = s.hashCode();
 
      System.out.println(x);
 
   } 
  }; 
  //其它代码. 
 }
 
 从代码上看,在一个方法内部定义的内部类的方 法访问外部方法内局部变量或方法参数,是非常自然的事,但内部类编译的时候如何获取这个变量,因为内部类除了它的生命周期是在方法内部,其它的方面它就是 一个普通类。那么它外面的那个局部变量或方法参数怎么被内部类访问?编译器在实现时实际上是这样的:


  public static void test(final String s){ 
     //或final String s = "axman";


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


即外部类的变量被作为构造方法的参数传给了内部类的私有成员. 
假如没有final,那么: 
 public static void test(String s){ 
     //或String s = "axman"; 
  ABSClass c = new ABSClass(){ 
   public void m(){ 
     s = "other"; 
   } 
  }; 
  System.out.println(s); 
 } 
 就会编译成: 
  public static void test(String s){ 
     //或String s = "axman";
  class OuterClass$1 extends ABSClass{
 
   private String s; 
   public OuterClass$1(String s){ 
      this.s = s;    
   } 
   public void m(){ 
     s = "other";
 
   } 
  }; 


   ABSClass c = new OuterClass$1 (s);
  }
 
 
 
 内部类的s重新指向"other"并不影响test的参数或外部定义的那个s.同理如果外部的s重新赋值内部类的s也不会跟着改变。 
 而你看到的 
  public static void test(String s){ 
     //或String s = "axman"; 
  ABSClass c = new ABSClass(){ 
   public void m(){ 
     s = "other"; 
   } 
  }; 
  System.out.println(s); 
 }
 
 在语法上是一个s,在内部类中被改变了,但结果打印的出来的你认为是同一的s却还是原来的"axman", 
 你能接收这样的结果吗? 
 所以final从语法上约束了实际上两个不同变量的一致性(表现为同一变量).


4.jdk1.8版本下的新变化

1.8以后,并非不用是final的,而是在编译期间要求值不发生变化。在你的代码中,如果user的值变化了,就会出错。
编译如下代码:

  1. public class TestFinal 
  2. {
  3.     //这样代码就不能通过编译了
  4.     public void test( User user)//这里不用final User user,但是user不能改变
  5.     {
  6.         user = new User();//user变化了,所以报错
  7.         user.setName("zhaoyang");
  8.         (new Thread()
  9.             { 
  10.                 public void run()
  11.                     {   
  12.                         System.out.println("user.name-->"+user.name);//报错
  13.                     }
  14.             } 
  15.         ).start();  
  16.     }
  17.     public static void main(String[] args) 
  18.     {
  19.         User user=new User();
  20.         user.setId(007);
  21.         user.setName("zhaoyang");
  22.          
  23.         TestFinal testFinal=new TestFinal();
  24.         testFinal.test(user);   
  25.     }
  26. }