深入理解 JAVA 内部类 - final 问题

前言:对于 JAVA 基础的内容,其实网上资料挺多的,本打算只写 Android 相关技术文章,但对于内部类这个基础点,在春招的准备过程中,发现里面有些门道而且很少人深入研究,特此贡献给大家

深入理解 JAVA 内部类系列文章如下:

  • 深入理解 JAVA 内部类 内部类的使用和分类
  • 深入理解 JAVA 内部类 内部类的 final 问题

本文主要讨论是内部类的 final 问题

阅读本文大概需要 8 分钟,大家耐心点看下去,会有不一样的收获哈~

使用到 final 的地方

因为使用内部类而出现需要使用 final 修饰符主要的有两个地方:

  1. 在内部类的方法使用到方法中定义的局部变量,则该局部变量需要添加 final 修饰符
  2. 在内部类的方法形参使用到外部传过来的变量,则形参需要添加 final 修饰符

其实这两种情况本质是一样的

那为什么局部变量需要用到 final 修饰符呢,我们来假想一种情况:

当我们创建匿名内部类的那个方法调用运行完毕之后,因为局部变量的生命周期和方法的生命周期是一样的,当方法弹栈,这个局部变量就会消亡了,但内部类对象可能还存在。 此时就会出现一种情况,就是我们调用这个内部类对象去访问一个不存在的局部变量,就可能会出现空指针异常

那使用 final 的意义

如果使用 final 修饰会在类加载的时候进入常量池,即使方法弹栈,常量池的常量还在,也可以继续使用,JVM 会持续维护这个引用在回调方法中的生命周期

网上大多这样解释,易理解

但是 JDK 1.8 竟然取消了对 final 的检查,什么情况,难道这种情况是程序员想多了?

我抱着质疑的角度深入查看,此处给出一个例子进行探索

例子

大家观察下我写的程序例子代码:

public class Outer {

    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.test1();
        outer.test2("165");
    }

    public static class Inner {
        public void print() {
        }
    }

//1. 在内部类的方法使用到方法中定义的局部变量,则该局部变量需要添加 final 修饰符
    public void test1() {
        final int temp = 1;
        Inner inner = new Inner() {
            public void print() {
                System.out.println(temp);
            }
        };
    }

//2. 在内部类的方法形参使用到外部传过来的变量,则形参需要添加 final 修饰符
    public void test2(final String s) {
        Inner inner = new Inner() {
            public void print() {
                System.out.println(s);
            }
        };
    }
}

大家猜猜编译后有多少个 .class 文件?

是 4 个,因为 Outer 类会生成 .class 文件,Inne r类会生成 .class 文件,还有两个匿名内部类也会分别生成一个 .class 文件

匿名内部类在实际编译的时候,会被编译成 Outer$Inner.class 字节码,上述代码假如在 Eclipse 下,你可以从项目文件夹中看到 bin 文件夹中看到有 4 个文件,分别是

Outer.class Outer$Inner.class Outer$1.class Outer$2.class

这说明内部类所处的等级和外部类中的等级处在同一个级别上。

通过编译看 final 的处理情况

此处给出编译后 Outer.class 的代码:

import java.io.PrintStream;

public class Outer
{
  public static void main(String[] args)
  {
    Outer outer = new Outer();
    outer.test1();
    outer.test2("165");
  }

  public void test1() {
    int temp = 1;
    Outer.Inner inner = new Outer.Inner()  {
      public void print() {
        System.out.println(1);
      } } ;
  }

  public void test2(String s) {
    Outer.Inner inner = new Outer.Inner(s) { // 有没有发现这里有点不一样
      public void print() {
        System.out.println(this.val$s); //有没有发现这里有点不一样
      }
    };
  }

  public static class Inner
  {
    public void print() {
    }
  }
}

仔细观察 test2 方法,看 new Outer.Inner(s) 有没有意思怪异,没错字符串 s 竟然成为了构造函数的参数了。然后再观察方法中使用它的地方变成了 this.val$s ,这是什么意思呢? 这其实是指这个变量已经变成了自己的内部的一个属性了

进而可以深入了解到

其实局部内部类并不是直接调用方法传进来的参数,而是内部类将传进来的参数通过自己的构造器备份到了自己的内部,自己内部的方法调用的实际是自己的属性而不是外部类方法的参数。
外部类中的方法中的变量或参数只是方法的局部变量,这些变量或参数的作用域只在这个方法内部有效,所以方法中被 final 的变量的仅仅作用是表明这个变量将作为内部类构造器参数,其实不加也可以,加了可能还会占用内存空间,影响 GC。

总结:需要使用 final 去持续维护这个引用在回调方法中的生命周期这种说法应该是错误的,也没必要。

最后

大概内容就是这些,表述可能有些不清晰,博主功力不深厚,假如有问题可以提出,我会好好研究并修改,多多见谅

你可能感兴趣的:(深入理解 JAVA 内部类 - final 问题)