内部类是一种非常有用的特性,它允许我们把一些逻辑相关的类组织在一起,并控制位于内部的类的可见性。然而内部类与组合是完全不同的概念,它了解外部类,并能与之通信,而且用内部类写出的代码更加优雅而清晰,尽管并不总是这样(Java 8 的 Lambda 表达式和方法引用减少了编写内部类的需求)。
1. 创建内部类
很简单,就是把类的定义置于外部类的里面:
public class Test {
class Test2 {
private int i;
Test2(int num) {
i = num;
}
int value() {
return i;
}
}
public Test2 test2(int num) {
return new Test2(num);
}
public static void main(String[] args) {
Test t1 = new Test();
Test.Test2 t2 = t1.test2(1);
System.out.println(t2.value());
}
}
输出结果:
如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须像在 main() 方法中那样,具体地指明这个对象的类型:OuterClassName.InnerClassName。(注:在外部类的方法中也可以直接指明类型 InnerClassName,在其他类中需要指明 OuterClassName.InnerClassName。)
2. 链接外部类
当生成一个内部类的对象时,此对象与制造它的外部对象之间就有了一种联系,内部类拥有其外部类的所有元素的访问权(包括 private 字段)。
当某个外部类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外部类对象的引用。然后,在访问此外部类的成员时,就是用那个引用来选择外部类的成员(由编译器处理所有的细节)。内部类的对象只能在与其外部类的对象相关联的情况下才能被创建(内部类是非 static 类时)。
3. 使用 .this 和 .new
如果需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和 this。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并受到检查,因此没有任何运行时开销。使用 .this 的代码示例:
public class DotThis {
void f() { System.out.println("DotThis.f()"); }
public class Inner {
public DotThis outer() {
return DotThis.this;
}
}
public Inner inner() { return new Inner(); }
public static void main(String[] args) {
DotThis dt = new DotThis();
DotThis.Inner dti = dt.inner();
dti.outer().f();
}
}
输出结果:
有时可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,必须在 new 表达式中提供对其他外部类对象的引用,这时需要使用 .new 语法:
public class DotNew {
public class Inner {}
public static void main(String[] args) {
DotNew dn = new DotNew();
DotNew.Inner dni = dn.new Inner(); // 在外部类的方法里,也可以直接用Inner引用
}
}
在拥有外部类对象之前是不可能创建内部类对象的,这是因为内部类对象会暗暗地连接到建它的外部类对象上。但是,如果创建的是静态内部类(嵌套类),那么它就不需要对外部类对象的引用(在上例 main 方法中改为 DotNew.Inner dni = new Inner();即可)。
4. 内部类与向上转型
当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。这是由于此内部类-某个接口的实现-能够完全不可见,并且不可用,所得到的只是指向基类或接口的引用,因而能够很方便地隐藏实现细节。
内部类可以是 private 或 protected,而外部类只能设置为 public 或 包访问权限。
如果一个内部类是 private 的,那只有在其所处的外部类中能访问。通过 private 内部类这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给 Java 编译器提供了生成高效代码的机会。
5. 内部类方法和作用域
可以在一个方法里面或者在任意的作用域内定义内部类。如果一个类被定义在方法作用域内,就意味着一旦该方法执行完,这个类便不可用了。
类也可以被定义在任意的作用域内,如定义在 if 语句的作用域内,出了该作用域就不能使用此类了。
6. 匿名内部类
代码示例:
interface TestAnonymous { // 这里也可以定义一个TestAnonymous类
int value();
}
public class Test {
public TestAnonymous testAnonymous() {
return new TestAnonymous() {
private int i = 1;
@Override
public int value() {
return i;
}
}; // 注意这里有个分号,表示return语句的结束
}
}
testAnonymous() 方法将返回值的生成与表示这个返回值的类的定义结合在了一起,并且这个类是匿名的,它没有名字。通过 new 表达式返回的引用被自动向上转型为对 TestAnonymous 的引用。
上述匿名内部类的语法是下述形式的简化:
interface TestAnonymous { // 这里也可以定义一个TestAnonymous类
int value();
}
public class Test2 {
class MyTestAnonymous implements TestAnonymous {
private int i = 1;
@Override
public int value() {
return i;
}
}
public TestAnonymous testAnonymous() {
return new TestAnonymous();
}
}
上述这个匿名内部类中,使用了默认的构造器来生成 TestAnonymous。下面的代码是,如果基类需要一个有参数的构造器,应该怎么办:
class TestAnonymous2 {
private int i;
public TestAnonymous2(int x) {
i = x;
}
public int value() {
return i;
}
}
public class Test3 {
public TestAnonymous2 testAnonymous2(int x) {
return new TestAnonymous2(x) {
@Override
public int value() {
return super.value();
}
};
}
public static void main(String[] args) {
System.out.println(new Test().testAnonymous(1).value());
}
}
输出结果:
此外,如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是 final 的(它在初始化后不会改变,即使不加 final, Java 8 的编译器也会自动加上 final,以保证数据的一致性)。
如果变量是被传递给匿名类的基类的构造器,不在匿名类内部被直接使用,就不要求变量一定是 final 的。
7. 嵌套类(静态内部类)
如果不需要内部类对象与其外部类对象之间有联系,那么可以将内部类声明为 static。普通的内部类对象隐式地保存了一个引用,指向创建它的外部类对象。然而,当内部类是 static 的时,就不是这样了。嵌套类意味着:
- 创建嵌套类的对象并不需要其外部类的对象。
- 不能从嵌套类的对象中访问非静态的外部类对象。
嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类,但是嵌套类可以包含所有这些东西。
在一个普通的(非 static)内部类中,通过一个特殊的 this 引用可以链接到其外部类对象。嵌套类没有这个特殊的 this 引用,这使得它类似于一个 static 方法。
接口内部的类
嵌套类可以作为接口的一部分,放到接口中的任何类都自动是 public 和 static 的,甚至可以在内部类中实现其外部接口,代码示例:
public interface MyInterface {
void method();
class Test implements MyInterface {
@Override
public void method() {
System.out.println(123);
}
public static void main(String[] args) {
new Test().method(); // 输出结果为123
}
}
}
如果想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便。
从多层嵌套类中访问外部类的成员
一个内部类被嵌套多少层并不重要——它能透明地访问所有它所嵌入的外部类的所有成员,如下所示:
class MNA {
private void f() {}
class A {
private void g() {}
public class B {
void h() {
g();
f();
}
}
}
}
public class MultiNestingAccess {
public static void main(String[] args) {
MNA mna = new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
}
可以看到在 MNA.A.B 中,调用方法 g() 和 f() 不需要任何条件(即使它们被定义为 private)。".new"语法能产生正确的作用域,所以不必在调用构造器时限定类名。
8. 为什么需要内部类
使用内部类最吸引人的原因是:每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
除此之外,如果使用内部类,还可以获得其他一些特性:
- 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外部类对象的信息相互独立。
- 在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。
- 内部类并没有令人迷惑的“is-a”关系,它就是一个独立的实体。
闭包与回调
闭包是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外部类中创建内部类的作用域的信息,还自动拥有一个指向此外部类对象的引用,在此作用域内,内部类有权操作所有的成员,包括 private 成员。
在 Java 8 之前,内部类是实现闭包的唯一方式,在 Java 8 中,我们可以使用 lambda 表达式来实现闭包行为,并且语法更加优雅和简洁。
人们认为 Java 应该包含某种类似指针的机制,以允许回调。通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。回调的价值在于它的灵活性-可以在运行时动态地决定需要调用什么方法。例如,在图形界面实现 GUI 功能的时候,到处都用到回调。
9. 继承内部类
因为内部类的构造器必须连接到指向其外部类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外部类对象的“秘密的”引用必须被初始化,而在派生类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:
class WithInner {
class Inner {}
}
public class InheritInner extends WithInner.Inner {
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
必须在构造器内使用如下语法:
enclosingClassReference.super();
这样才提供了必要的引用,然后程序才能编译通过。
10. 局部内部类
可以在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符,因为它不是外部类的一部分;但是它可以访问当前代码块内的常量,以及此外部类的所有成员。
public class Test {
public void methed2() {
int i = 1;
class Test3 {
void method3() {
// i = 2; 不能修改
System.out.println("i = " + i);
}
}
}
}
内部类中引用外面的局部变量必须是 final 的(Java 8开始,如果局部变量声明并初始化后没有被修改过,则此时该变量也会被当成是final的(称为effictively final))。
参考资料:On Java 8