内部类

2.为什么局部内部类和匿名内部类只能访问局部final变量?

想必这个问题也曾经困扰过很多人,在讨论这个问题之前,先看下面这段代码:
public class Test {
    public static void main(String[] args)  {
   
    }
     
    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
} 
这段代码会被编译成两个class文件:Test.class和Test1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outterx.class(x为正整数)。

根据上图可知,test 方法中的匿名内部类的名字被起为 Test$1。
上段代码中,如果把变量 a 和 b 前面的任一个 final 去掉,这段代码都编译不过。我们先考虑这样一个问题:
当 test 方法执行完毕之后,变量a的生命周期就结束了,而此时 Thread 对象的生命周期很可能还没有结束,那么在 Thread 的 run 方法中继续访问变量 a 就变成不可能了,但是又要实现这样的效果,怎么办呢?Java 采用了复制的手段来解决这个问题。将这段代码的字节码反编译可以得到下面的内容:

我们看到在 run 方法中有一条指令:
bipush 10
这条指令表示将操作数10压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。
下面再看一个例子:
public class Test {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int a) {
        new Thread(){
            public void run() {
                System.out.println(a);
            };
        }.start();
    }
}
反编译得到:

我们看到匿名内部类 Test$1 的构造器含有两个参数,一个是指向外部类对象的引用,一个是 int 型变量,很显然,这里是将变量 test 方法中的形参 a 以参数的形式传进来对匿名内部类中的拷贝(变量 a 的拷贝)进行赋值初始化。
也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。
从上面可以看出,在 run 方法中访问的变量 a 根本就不是 test 方法中的局部变量 a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在 run 方法中访问的变量 a 和 test 方法中的变量 a 不是同一个变量,当在 run 方法中改变变量 a 的值的话,会出现什么情况?
对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java 编译器就限定必须将变量 a 限制为 final 变量,不允许对变量 a 进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。
到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用 final 进行限定了。
3.静态内部类有特殊的地方吗?
从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译 class 文件看一下就知道了,是没有 Outter this&0 引用的。

posted on 2021-07-20 17:28  辰逸1  阅读(38)  评论(0编辑  收藏  举报