Java 内部类详解

标签(空格分隔): java


成员内部类

在java中允许一个类的内部定义另一个类,称为内部类(inner class),或者嵌套类(nested class).内部类和外层的类存在逻辑上的所属关系。

内部类的特性:

  • 内部类可以访问外部类的数据,包括私有数据
  • 内部类可以对同一个包中的其他类隐藏起来

下面就是一个内部类示例:

public class Outer {
    private String outerName;

    public Outer() {
        outerName = "abc";
    }

    class Inner {
        private String innerName;

        public Inner() {
            innerName = outerName;
        }
    }
}

上面这段代码定义了一个外部类Outer,它包含了一个内部类Inner,编译会生成两个.class文件:Outer.classOuter$Inner.class,表明了内部类会编译成独立的字节码文件。内部类是一种编译器现象,与虚拟机无关。编译器将会把内部类翻译成用 $ 符号分隔外部类名与内部类名的常规类文件,而虚拟机则对此一无所知。

因为内部类可以直接访问外部类的成员变量,因此必须要现有外部类对象,才能生成内部类对象

import cn.outer.Outer.Inner;
public class Test {
    private Inner inner;
    public Test() {
        //编译出错,无法直接实例化内部类对象
        //inner = new Inner();
        
        //正确初始化内部内对象的方式
        Outer outer = new Outer();
        inner = outer.new Inner();
    }
}

当内部类的访问权限可以是public, 默认,protected, private的,而外部类却只能是public 或者默认的访问权限。内部类的访问权限和外部类的成员访问权限具有一样的表现。

内部类能够直接访问外部类的成员属性,这个是如何做到的呢?
我们可以反编译Outer$Inner.class这个字节码文件得到的结果如下:

 javap '.\Outer$Inner.class'
 class cn.outer.Outer$Inner {
  final cn.outer.Outer this$0;
  public cn.outer.Outer$Inner(cn.outer.Outer);
}

可以看到内部类里存在一个外部类的对象。

静态内部类

内部类可以声明成static的,这样内部类就成为了类级别,无法直接访问外部类的非静态成员。
同时非静态的内部类,不能声明静态的成员属性,如果非要这样做,则必须在追加final声明。静态的内部类对象和外部类对象间不存在依赖关系,可以直接创建。静态内部类不会持有外部类的引用。

import cn.outer.Outer.Inner;
public class Test{
    private Inner inner;
    public Test() {
        inner = new Inner();
    }
}

局部内部类和匿名内部类

public void func() {
        int age = 3;
        class Inner{
            public void printAge() {
                System.out.println(age);
            }
        }
        new Inner().printAge();
    }

定义在代码块中,如果局部内部类使用外部局部变量,则需要加final关键字,当然在1.8之后已经成为编译器的默认行为。

匿名内部类是局部内部类的特殊形式,没有变量名指向这个类的实例。一般匿名内部类必须继承一个父类或者实现一个接口。

我们在编程中经常看到这样的操作,匿名内部类中访问外部局部变量需要添加final关键字,而访问成员变量则不需要添加。否则编译就无法通过。(1.8之后,局部变量也不需要添加final关键字了,这已经成为编译器的默认行为),那么为什么编译器要这么做,这里面隐藏了哪些细节呢?

如果用一个高大上的名词来形容这个特性,我们可以叫它:闭包,这里我并不打算解释什么是闭包,我们只需要弄懂上面final问题就行了。看一下面的例子

public class Test {

    public static void main(String[] args) {
        Test t = new Test();
        for (int i = 0; i < 10; i++) {
            setCallback(t, i);
        }
    }

    public static void setCallback(Test t, int key) {
        final int key1 = key;
        final int key2 = key + 1;

        t.setListener(new OnClickListener() {

            @Override
            public void onClick() {
                int res1 = key1;
                int res2 = key2;
            }
        });
    }

    public void setListener(OnClickListener listener) {
        listener.onClick();
    }

    interface OnClickListener {
        void onClick();
    }
}

重点看setCallback函数,我们看到这个函数里有一个匿名内部类监听,在这个监听器里使用key1key2两个变量,这两个变量从代码上看是setCallback里的局部变量,因此要加上final关键字修饰,有没有想过这个如果这个onClick方法出现调用延时,则key1的值还会是当初设置给他的那个原始值吗?经验告诉我们还是原来设置给它的那个初始值。给人感觉好像key1和这个匿名对象绑定了似的。

想要探究这个问题,我们光靠经验是不够的,最直观的表现就是看虚拟机是如何解释这段代码的,查看这个类编译后的.class文件,这个文件会生成三个.class文件,分别是Test.class这个不用多说,OnClickListener这个接口属于内部接口也会生成一个Test$OnClickListener.class ,还有一个匿名的class文件,是我们实现OnClickListener这个接口生成的,这个类是匿名的,在编译器的作用下会生成一个这样的文件Test$1.class,我们需要查看的就是这个文件。

// Compiled from Test.java (version 1.8 : 52.0, super bit)
class cn.sivin.sfile.Test$1 implements cn.sivin.sfile.Test$OnClickListener {
  
  // Field descriptor #8 I
  private final synthetic int val$key1;
  
  // Field descriptor #8 I
  private final synthetic int val$key2;
  
  // Method descriptor #11 (II)V
  // Stack: 2, Locals: 3
  Test$1(int arg0, int arg1);
     0  aload_0 [this]
     1  iload_1 [arg0]
     2  putfield cn.sivin.sfile.Test$1.val$key1 : int [13]
     5  aload_0 [this]
     6  iload_2 [arg1]
     7  putfield cn.sivin.sfile.Test$1.val$key2 : int [15]
    10  aload_0 [this]
    11  invokespecial java.lang.Object() [17]
    14  return
      Line numbers:
        [pc: 0, line: 16]
      Local variable table:
        [pc: 0, pc: 15] local: this index: 0 type: new cn.sivin.sfile.Test(){}
  

我们可以看到编译器其实为我们生成了一个Test$1这个类,这个类里有两个成员val$key1valkey2,同时也为我们生成了两个参数的构造函数,这实际上相当于外部的局部变量通过构造函数传递到这个匿名对象中持有了,我们在onClick函数中使用的key1key2实际上是这个匿名对象内部的成员key1key2,那么我们为什么不能修改这两个值呢?这就涉及引用传递和值传递,从理论上我们可以修改这个值,只不过修改的结果不会作用到外部的key1,和key2上,这就会给开发人员造成困扰,于是java就将这个外部局部变量给定义成final,意思就是说,你别改我了,改了也没用。从编译后的字节码文件看这个类里的成员变量也是final的,这也从语法上验证了这一点。

那么我们却可以在onClick中更改成员变量的值,为什么呢?

这里我们需要修改一下代码,因为setCallback是一个静态的函数,其只能调用static的成员变量。我们将其改成成员函数,在编译一下看看。为了简洁,click里我们什么也不做。

private String name = "bac";
public void setCallback(Test t, int key) {

    t.setListener(new OnClickListener() {

        @Override
        public void onClick() {
            name = "sdf";
        }
    });
}

查看字节码文件:

// Compiled from Test.java (version 1.8 : 52.0, super bit)
class cn.sivin.sfile.Test$1 implements cn.sivin.sfile.Test$OnClickListener {
  
  // Field descriptor #8 Lcn/sivin/sfile/Test;
  final synthetic cn.sivin.sfile.Test this$0;
  
  // Method descriptor #10 (Lcn/sivin/sfile/Test;)V
  // Stack: 2, Locals: 2
  Test$1(cn.sivin.sfile.Test arg0);
     0  aload_0 [this]
     1  aload_1 [arg0]
     2  putfield cn.sivin.sfile.Test$1.this$0 : cn.sivin.sfile.Test [12]
     5  aload_0 [this]
     6  invokespecial java.lang.Object() [14]
     9  return
      Line numbers:
        [pc: 0, line: 16]
      Local variable table:
        [pc: 0, pc: 10] local: this index: 0 type: new cn.sivin.sfile.Test(){}
  

我们可以看到编译后的类里有一个外部类的成员变量,final synthetic cn.sivin.sfile.Test this$0,这个成员是有构造函数传递过来的。

我们在看看调用:

 // Method descriptor #16 ()V
  // Stack: 2, Locals: 1
  public void onClick();
     0  aload_0 [this]
     1  getfield cn.sivin.sfile.Test$1.this$0 : cn.sivin.sfile.Test [12]
     4  ldc  [22]
     6  invokestatic cn.sivin.sfile.Test.access$0(cn.sivin.sfile.Test, java.lang.String) : void [24]
     9  return
      Line numbers:
        [pc: 0, line: 20]
        [pc: 9, line: 21]
      Local variable table:
        [pc: 0, pc: 10] local: this index: 0 type: new cn.sivin.sfile.Test(){}

在第1到第4这这两步中,我们清楚的看到,实际上是通过外部类的对象来修改name这个属性的值。
这也是值传递和引用传递的特性表现。同时也证明了,非静态匿名对象会持有外部类对象实例的说法。静态内部类不会持有外部类对象,因为它可以独立构成对象,不需要依赖外部类对象实例。

你可能感兴趣的:(Java 内部类详解)