【Java编程思想】10.内部类
将一个类的定义放在另一个类的定义内部,这就是内部类。
10.1 创建内部类
- 内部类的名字是嵌套在外部类里面的
- 外部类可以有方法,返回一个指向内部类的调用。(外部类中可以调用内部类)
- 如果在外部类中,希望能在除了静态方法之外的任意位置创建某个内部类对象,那么可以向下面这样指明对象类型。
OuterClassName.InnerClassName x = new InnerClassName();
10.2 链接到外部类
在创建了一个内部类的对象后,内部类与制造它的外围对象(enclosing object,其实就是其所属于的外部类)之间就有了一种联系。这时内部类可以访问外围对象的所有成员,且不需要任何特殊条件;内部类还拥有其外围类的所有元素的访问权。
换一种说法,就是内部类可以访问外围类的方法和字段,就好像自己拥有这些方法和字段一样。
内部类拥有外围类所有成员的访问权的原因:
- 某个外围类的对象创建了一个内部类对象。
- 内部类对象会秘密捕获一个指向外围类对象的引用。
- 内部类访问外围类成员时,实际使用的就是这个引用。
- 因此构建内部类对象时,需要一个指向外围类对象的引用,如果编译器找不到该引用就会报错
回到上一章节提到的 tips,为什么内部类的对象只能与外围类的对象相关联的情况下才能被创建(在内部类是非静态类时),为什么在外部类中的静态方法创建内部类对象时需要
OuterClassName.InnerClassName
这样的声明。也是因为静态区本身是独立的,
10.3 使用 .this 和 .new
如果需要生成对外部类对象的引用,可以使用 OuterClassName.this
的形式。这样产生的引用自动地具有正确的类型(这一点在编译期就会被确认,因此节省了运行时开销)。
如果需要让外围类去创建其某个内部类的对象,可在 new 表达式中提供对其他外部类对象的引用,需要 .new
语法,如下:
OuterClassName x = new OuterClassName();
OuterClassName.InnerClassName y = x.new InnerClassName();
也就是说,想直接创建对内部类对象,必须使用外部类的对象来创建内部类的对象。因此,也可以说,在拥有外部类对象之前是不可能创建内部类对象的(原因见上一章)。
但是如果创建的是嵌套类(静态内部类),就不需要对外部类对象的引用。
10.4 内部类与向上转型
当将内部类向上转型为基类,尤其是转型为一个接口的时候,内部类-->某个接口的实现-->可以完全不可见,并且不可用。所能得到的只是指向基类或者接口的引用,这样就将实现细节隐藏起来了。
实现某个接口的对象,从而得到该接口的引用=将内部类向上转型为基类
使用内部类去继承类,或是实现接口,可以很好的阻止外部的访问,隐藏实现细节,阻止任何依赖于类型的编码。
10.5 在方法和作用域内的内部类
可以在一个方法里,或者任意的作用域内定义内部类,原因如下:
- 实现了某个类型的接口,就可以创建并返回对其的应用。
- 要解决复杂的问题,需要创建一个类辅助,但是不希望这个类是公共可用的。
内部类还有其他使用方式,包括:
- 一个定义在方法中的类。
在方法的作用域内(而不是其他类的作用域内)创建一个完整的类,被称为局部内部类。 - 一个定义在作用域内的类,此作用域在方法内部。
像这类内部类,仅能作用在对应的作用域之内,除此之外与普通类一致。 - 一个实现了接口的匿名类。
- 一个匿名类,拓展了有非默认构造器的类。
- 一个匿名类,执行字段初始化。
- 一个匿名类,通过实例初始化实现构造(匿名类不能有构造器)。
10.6 匿名内部类
创建一个实现了接口的内部类:
public Contents contents() {
return new Contents() { // Insert a class definition
private int i = 11;
@Override
public int value() { return i; }
}; // Semicolon required in this case
}
创建一个使用有参数构造器的基类的匿名内部类:
public Wrapping wrapping(int x) {
// Base constructor call:
return new Wrapping(x) { // Pass constructor argument.
@Override
public int value() {
return super.value() * 47;
}
}; // Semicolon required
}
匿名内部类末尾的分号,并不是用来标记次内部类结束的。实际上他标记的是表达式的结束,只不过表达式正好包含内部类而已。
创建执行字段初始化的匿名内部类:
public Destination destination(final String dest) {
return new Destination() {
private String label = dest;
@Override
public String readLabel() {
return label;
}
};
}
在匿名内部类中,使用一个在其外部定义的对象时,编译器会要求其参数引用时 final,就跟 Java8中 lambda 表达式中的引用外部参数一样。否则编译会报错。
创建通过实例初始化实现构造器效果的匿名内部类:
public Destination destination(final String dest, final float price) {
return new Destination() {
private int cost;
// Instance initialization for each object:
{
cost = Math.round(price);
if(cost > 100)
System.out.println("Over budget!");
}
private String label = dest;
@Override
public String readLabel() {
return label;
}
};
}
对于匿名类而言,实例初始化的实际效果就是构造器(当然是受到了限制-->不能重载实例初始化方法,仅仅是拥有这样一个勾构造器)
匿名内部类既可以继承拓展类,也可以实现接口(只能实现一个接口),但是不能两者兼备。
有了内部类,可以尝试再次实现第九章中的工厂方法:
interface Service {
void method1();
void method2();
}
interface ServiceFactory {
Service getService();
}
class Implementation1 implements Service {
private Implementation1() {}
@Override
public void method1() {print("Implementation1 method1");}
@Override
public void method2() {print("Implementation1 method2");}
public static ServiceFactory factory = new ServiceFactory() {
public Service getService() {
return new Implementation1();
}
};
}
class Implementation2 implements Service {
private Implementation2() {}
@Override
public void method1() {print("Implementation2 method1");}
@Override
public void method2() {print("Implementation2 method2");}
public static ServiceFactory factory = new ServiceFactory() {
public Service getService() {
return new Implementation2();
}
};
}
public class Factories {
public static void serviceConsumer(ServiceFactory fact) {
Service s = fact.getService();
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer(Implementation1.factory);
// Implementations are completely interchangeable:
serviceConsumer(Implementation2.factory);
}
}
与之前的工厂方法相比,用于 Implementation1
和 Implementation2
的构造器都可以是 private 的,并且没有任何必要去创建作为工厂的实现类。另外从来只需要单一的工厂对象。
10.7 嵌套类
如果不需要内部类对象与外围类之间有联系,则可以将内部类声明为 static,这就是嵌套类,
对于普通内部类:
- 普通内部类对象隐式的保存了其外围类对象的引用
- 普通内部类的字段与方法,只能放在类的外部层次上,因此普通内部类不能有 static 数据和字段,也不能包含嵌套类。
而嵌套类:
- 要创建嵌套类的对象,并不需要其外围类对象。
- 不能从嵌套类的对象中访问非静态的外围类对象。
- 嵌套类可以包含 static 数据和字段,
关于嵌套类还有如下几种使用方式:
- 接口内部的嵌套类:
正常情况下不能在接口内部放置任何实现代码,但是嵌套类可以作为接口的一部分(放在接口中的任何类都自动式 public 和 static 的),甚至可以在嵌套内部类中实现外围接口。 - 从多层嵌套类中访问外部类的成员:
一个内部类被嵌套多少次不重要-->这个内部类可以透明的访问所有它所嵌入的外围类的所有成员。
10.8 为什么需要内部类
内部类实现一个接口与外围类实现一个接口的区别在于:后者不是总能享用到接口带来的方便,有时需要用到接口的实现。
所以可以得出一个结论:每个内部类都能独立的继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。这是内部类最吸引人的特性!
这段其实就是说,无论外围类怎么搞怎么玩,我都能用外围类里面的内部类额外的单独去实现一个特定的接口。这个特性在外围类已经继承抽象类或具体类的时候,去实现多重继承时特别有用。
内部类的一些额外特性:
- 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。
- 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。
- 创建内部类对象的时刻并不依赖于外围类对象的创建(
那外围类的引用咋整呢。。。创建内部类只需要外围类对象的引用,是引用就成) - 内部类并没有“is-a”关系,它只是一二个独立的个体。
关于闭包与回调
闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。
按照这样的定义,可以说内部类是面向对象的闭包(Java 并没有显式的支持闭包),因为它不仅包含外围类独享的信息,还自动拥有一个指向此外围类对象的引用。
通过内部类可以实现类似其他语言的指针机制带来的回调功能,通过回调,对象可以携带一些信息,这些信息允许该对象在稍后的某个时候调用初始的对象。
我们可以简单的把闭包理解为“一块代码可以传入另一个地方,并且在终点处可以运行该代码”,用 Java 语言来描述就是“可以把一个类对象打包传给另一个类对象里。
interface Incrementable {
void increment();
}
class MyIncrement {
public void increment() {
print("Other operation");
}
static void f(MyIncrement mi) {
mi.increment();
}
}
class Callee2 extends MyIncrement {
private int i = 0;
public void increment() {
super.increment();
i++;
print(i);
}
private class Closure implements Incrementable {
public void increment() {
// Specify outer-class method, otherwise you'd get an infinite recursion:
Callee2.this.increment();
}
}
Incrementable getCallbackReference() {
return new Closure();
}
}
class Caller {
private Incrementable callbackReference;
Caller(Incrementable cbh) {
callbackReference = cbh;
}
void go() {
callbackReference.increment();
}
}
public class Callbacks {
public static void main(String[] args) {
Callee2 c2 = new Callee2();
MyIncrement.f(c2);
Caller caller2 = new Caller(c2.getCallbackReference()); // 展示回调
caller2.go();
caller2.go();
}
}
输出:
Other operation
1
Other operation
2
Other operation
3
Callee2
继承了 MyIncrement
,就不能为了 Incrementable
的用途而覆盖 increment()
方法,于是使用内部类独立实现 Incrementable
接口的方法。这么做的同时没有修改外围类的接口。
内部类 Closure
实现了 Incrementable
,以提供一个返回 Callee2
的“钩子”(hook)。且这个钩子返回制定了规则:无论谁获得 Incrementable
的引用,都只能调用 increment()
方法,除此之外没有其他功能。
Caller
的构造器需要一个 Incrementable
的引用做参数,然后在以后的某个时候,Caller
对象可以使用此引用回调 Callee
类。
回调其实就是,A类 调用 B类 中的方法 b,然后 B 类反过来调用 A 类中的方法 a,那么方法 a 就是回调方法。具体实现上各有差异,一般都用在像线程啊,消息处理这块。回调的价值就在于,可以在运行时动态的决定需要调用什么方法。
之所以在内部类这部分提到回调,就是因为 Java 这种仿闭包的非静态内部类(记录外部类的详细信息;保留外部类对象的引用;可以直接调用外部类任意成员),可以很方便的实现回调功能--->在某个方法获得内部类对象的引用后,反过来直接调用外围类的方法,这也是回调的表现形式。
内部类与控制框架
应用程序框架(application framework)就是被设计泳衣解决某些特定问题的一个类或一组类。
控制框架是一类特殊的应用程序框架,用来解决响应事件的需求。主要用来响应事件的系统被称作事件驱动系统。
在这类设计中,关键的点在于需要“使变化的事务和不变的事物相互分离”。
内部类允许:
- 控制框架的完整实现是由单个的类创建的,从而使实现的细节被封装了起来。内部类用来表示解决问题所必需的各种不同的
action()
。 - 内部类能够很容易的访问外围类成员。
10.9 内部类的继承
在继承内部类的时候,因为内部类的构造器必须链接到指向其外围类对象的引用,这个“秘密的”引用必须被初始化,在导出类中也不会再存在可连接的默认对象,因此在继承时需要使用下面的语法,描述清楚导出类,基类(内部类),外围类之间的关系。
class WithInner {
class Inner {}
}
public class InheritInner extends WithInner.Inner {
//! InheritInner() {} // Won't compile
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
可以看到,在生成一个构造器的时候,不能使用默认构造器(编译器会报错),需要在构造器内部使用 enclosingClassReference.super()
,提供必要的内部类引用。
10.10 内部类可以被覆盖吗
对于内部类来说,“覆盖”它就好像它是外围类的一个方法,但是并没有起到什么作用。
这种情况发生后,其实内部类和“覆盖”的内部类完全是两个独立的实体,各自在自己的命名空间内。
但是如果在继承内部类时,指定内部类的外围类对象的引用,那么就会明确继承的类,这样就跟正常的覆盖一样,重新实现对应方法即可。
10.11 局部内部类
局部内部类(例如在方法体内创建的类等)不能有访问说明符,因为他不是外围类的一部分。但是它可以访问当前代码块内的常量,以及此外围类的所有成员。
在实现上,局部内部类和匿名内部类是相似的,二者具有相同的行为和能力。
- 但是局部内部类是有名称的,所以可以有“带有命名”的构造器,也可以重载构造器。
- 而匿名内部类只能用于实例初始化。
- 同时使用局部内部类的时候,可以创建不止一个该内部类的对象。而匿名内部类只能在实例初始化的时候被创建一次。
10.12 内部类标识符
每个类都会产生一个 .class
文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个 meta-class,叫做 Class 对象。
因此内部类也必须生成一个 .class
文件,以包含他们的 Class 对象信息。这些类文件的命名有严格的规则,必须是外围类的名字。加上‘$’,再加上内部类的名字构成。
OuterClassName$InnerClassName.class
如果是匿名内部类,编译器会简单的产生一个数字作为其标识符。
如果是嵌套内部类,只需直接将他们的名字加在其外围标识符与‘\(’后面。
这种命名方式是纯粹的 Java 标准,与平台对‘\)’符号的设置没有关系。