为什么使用内部类?

在 『Effective Java』中提到了一条建议,尽量使类和成员的可访问性最小化。这个原则其实是面向对象的程序设计的基本原则,即封装的概念。对于成员变量,我们知道,可以有较好的访问权限控制;但是对于类来说,通常我们都使用 public 或 包级私有(默认,不加修饰符),这样的话似乎做不到可访问性的控制,顶多是不同包之间不能访问。其实,这只是针对顶层的类和接口而言。类和接口也可以嵌套在其他的类和接口中,被称为内部类(或嵌套类)。对于内部类而言,就可以和成员一样使用 private 等修饰符来控制可访问性。

如果一个类或接口只是在某一个类的内部被用到,就应该考虑使它成为唯一使用它的那个类的私有嵌套类。

内部类除了能够提供更好的封装以外,还具有一些特殊的性质,如内部类可以直接访问到外部类中的成员,包括私有的成员。

在 Java 的集合框架中大量地使用了内部类,比如集合类中迭代器的实现。这些迭代器实现了同样的接口(Iterator Interface),但是具体的实现又各不相同。外部类也不必关心它们的具体实现方法,只需要按照约定的接口进行访问即可。

内部类怎么用?

成员内部类

相当于外部类的一个成员的位置,可以使用任意的访问修饰符,如 public, protected, private 或是不加访问修饰符(包可见)。常规类只有包可见性或公有可见性。

成员内部类是最普通的内部类,它相当于外部类的一个成员,所以可以无限制的访问外部类的所有 成员属性和方法,即便是 private 的成员或方法也可以直接访问;但是外部类要访问内部类的成员属性和方法则需要通过内部类实例来访问。

在成员内部类中要注意两点。首先,成员内部类中不能存在任何 static 的变量和方法;其次,成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。实际上,内部类中存在一个外部类的引用,可以使用 OuterClass.this 来显示指代。

成员内部类中成员内部类中不能存在任何 static 的变量和方法,但 static final 的常量是可以的。 因为成员内部类是要依赖于外部类的实例,而静态变量和方法是不依赖于对象的,在加载静态域时外部类的实例尚未创建,自然不行了;但是常量是在编译期就确定的,放在常量池中。

内部类实例的创建需要使用 OuterClass.InnerClass inner = outer.new InnerClass(); 的形式创建。建议在外部内中提供一个创建内部类实例的方法,如:

1
2
3
public InnerClass getInnerClass() {
  return new InnerClass();
}

注意: 1. 外部类是不能直接使用内部类的成员和方法的,可先创建内部类的对象,然后通过内部类的对象来访问其成员变量和方法; 2. 如果外部类和内部类具有相同的成员变量或方法,内部类默认访问自己的成员变量或方法,如果要访问外部类的成员变量,可以使用 this 关键字,如OuterClass.this.name

在编译后会内部类会被编译为 Outer$Inner.class, 下面给个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Outer {
    private int id;
    private int state;

    public class Inner {
        private int field;

        public void displayOuterState() {
            System.out.println("Outer Id: " + id);
            System.out.println("Outer State: " + Outer.this.state);
        }

        public void setField(int field) {
            this.field = field;
        }

        public int getField() {
            return this.field;
        }
    }

    public static void main(String[] args) {
        Outer.Inner inner = new Outer().new Inner();
        inner.displayOuterState();
    }
}

反编译一下 Outer$Inner.class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
F:\Demos\InnerClass>javap -p Outer$Inner.class
Compiled from "Outer.java"
public class Outer$Inner {
  private int field;
  final Outer this$0;
  public Outer$Inner(Outer);
  public void displayOuterState();
  public void setField(int);
  public int getField();
}

其中 this$0 就是外部类的引用。

再反编译一下 Outer.class, 看到编译器在外部类添加了静态方法 access$000(Outer)access$100(Outer), 内部类正是通过这两个方法来读取到私有的外部成员变量的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
F:\Demos\InnerClass>javap -p Outer.class
Compiled from "Outer.java"
public class Outer {
  private int id;
  private int state;
  public Outer();
  public static void main(java.lang.String[]);
  static int access$000(Outer);
  static int access$100(Outer);
}

access$000 并不是一个合法的方法名,但如果熟悉类文件的结构的话还是可以通过创建虚拟机指令来调用的,进而获取到私有的成员。但是,注意到这个方法具有包可见性。要想通过这种方式来访问私有成员,还是很有难度的。

静态内部类

静态内部类只是将一个类隐藏在一个类的内部,和外部类的对象没有任何关联,不像成员内部类那样会持有一个外部类的引用。最好将静态内部类看作一个普通类,只是碰巧被声明在另一个类的内部而已。静态内部类桶其他的静态成员一样,也遵循同样的可访问性规则。如果被声明为 private ,则使能在外部内中才能使用。

静态内部类中不能直接访问任何外围类的非 static 成员变量和方法;创建静态内部类的对象时不需要通过外部类的对象来进行。静态内部类可以直接访问外部类的静态成员,包括私有的。

声明在接口中的内部类自动成为 public 和 static 。

局部内部类(方法内部类)

局部内部类是嵌套在方法和作用域内的,就像是方法里面的一个局部变量一样。

局部内部类是不能有 public、protected、private 以及 static 等修饰符的,因为它的作用域被局限在声明这个局部类的块中。局部内部类最大的优势在于,它对外部世界可以完全地隐藏起来。除了它所在的作用域中,即便是外部类中的其它方法中也不能访问到它。

局部内部类除了可以访问外部类的成员以外,还可以访问它所在的作用域中的局部变量。不过,这些局部变量必须是被声明为 final 的才可以。有时候 final 的限制会很不方便,可以使用一个长度为1的数组类补救(数组变量虽然是被声明为final的,但这仅仅表示不可以让其引用另一个数组对象。数组中的数据元素还是可以自由改变的)。

匿名内部类

匿名内部类是对局部内部类的一种深化。如果只需要创建内部类的一个对象的话,其实就不必命名了。

匿名内部类的语法格式为 new SuperType(construction parameters) {}

如果 SuperType 是一个接口,则匿名类实现这个接口,并扩展 Object 类;如果 SuperType 是一个类,则匿名类是该类的子类。

匿名类没有名称,因而不能在类主体中定义构造器。定义匿名类时,如果在父类后面的括号中指定参数,会传递给父类的构造器。匿名类只有一个默认的构造器,但经常可以使用初始化语句块来代替构造方法。如“双括号初始化”技巧:

1
2
3
4
5
6
7
invite(new ArrayList<String>(){
    {
      add("Harry");
      add("Tony");
    }
  };
);

这里就利用了内部类的语法。外层括号创建了 ArrayList 的一个匿名子类, 内部括号则是一个初始化块。

匿名内部类在一些“回调”的场景中有大量应用,如 Android 开发中为控件设置监听器,就经常见到匿名内部类的用法。

这里分享一个 『Java 核心技术』 中提到的使用匿名内部类的一个场景。在生成日志或调试消息是,通常希望包含当前类的类名,如:

1
System.err.println("Something awful happened in :" + getClass());

但是,这对于静态方法不奏效,因为 getClass() 不是一个静态方法。可以通过以下技巧在静态方法中获取到所在的类:

1
new Object(){}.getClass().getEnclosingClass(); //get class of static method

在这里,会建立一个 Object 的匿名子类的对象,进而通过 getEnclosingClass 得到其外围类,及包含该静态方法的类。

Java 8 之后,很多使用匿名内部类的场景都可以使用 Lambda 表达式进行替换,使用起来也更为方便。

为什么局部类可访问的局部变量必须是 final 的?

局部类的实例在 JVM 退出定义了这个局部类的代码块后依然存在。即,即便这个类的局部定义,但这个类的实例能够跳出定义它的地方。

在 Java 中,局部类有时也被称为闭包。更一般的 Java 术语, 闭包是一个对象,它保存作用域的状态,并让这个作用域在后面可以继续使用。不同的编程语言中定义和实现闭包的方式都不一样, Java 通过局部类,匿名类和 lambda 表达式来实现闭包。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Weird {
  public static interface IntHolder {public int getValue(); }

  public static void main(String[] args) {
    IntHolder[] holders = new IntHolder[10];

    for (int i = 0; i < 10; i++) {
      final int fi = i;

      class MyIntHolder implements IntHolder {
        public int getValue() {
          return fi;
        }
      }

      holders[i] = new MyIntHolder();
    }

    //跳出了内部类定义的作用域,但内部类的实例仍然存在
    for (int i = 0; i < 10; i++) {
      System.out.println(holders[i].getValue());
    }
  }
}

实际上,在通过局部类访问外部的局部变量是并不是直接访问的,我们反编译一下内部类来看一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Weird$1MyIntHolder
    implements Weird.IntHolder
{

    public int getValue()
    {
        return val$fi;
    }

    final int val$fi;

    Weird$1MyIntHolder()
    {
        val$fi = i;
        super();
    }
}

可以看到,在局部内中有一个成员 final int val$fi; 是外部局部变量的引用拷贝(这里是基本类型,没有体现),后面通过局部类的实例访问时都是通过这个拷贝及进行的。为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用 final 来让该引用不可改变。

有时候这个限制会显得不太实用,可以使用一个长度为1的数组类补救(数组变量虽然是被声明为final的,但这仅仅表示不可以让其引用另一个数组对象。数组中的数据元素还是可以自由改变的)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
final int[] val = new Int[1];

class MyIntHolder implements IntHolder {
  public void add(int i) {
    val[0] += i;
  }
  public int getValue() {
    return val[0];
  }
}