第10章 内部类
可以将一个类的定义放到另一个类的定义内部,这就是内部类。内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可视性。然而必须要了解,内部类和组合是完全不同的概念。在最初,内部类看起来像是一种代码隐藏机制:将类置于其他类的内部。但是,你将会了解到,内部类远不止如此,它了解外部类,并能与之通信;而且你用内部类写出的代码更加优雅而清晰,尽管并不总是这样。
内部类的创建很简单,如定义的一样,将类置于外围类的内部。内部类的使用,与普通类也没什么不同,只是,内部类只能在外围类内部创建对象(new关键字创建)。可以在外围类里建立一个方法,该方法返回一个指向内部类的引用。如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须具体地指明这个对象的类型:OuterClassName.InnerClassName。内部类的对象只能在与其外围类的对象相关联的情况下才能被创建。
当生成一个内部类的对象时,此对象与制造它的外围对象(enclosing object)之间就有了一种联系,它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类的所有元素的访问权。下面的例子说明了这点:
package innerclass;
interface Selector{
boolean end();
Object current();
void next();
}
public class Sequence {
private Object[] items;
private int next = 0;
public Sequence(int size) {
items = new Object[size];
}
public void add(Object x) {
if(next < items.length) {
items[next++] = x;
}
}
private class SequenceSelector implements Selector{
private int i = 0;
public boolean end() {
return i == items.length;
}
public Object current() {
return items[i];
}
public void next() {
if(i < items.length) {
i++;
}
}
}
public Selector selector() {
return new SequenceSelector();
}
public static void main(String[] args) {
Sequence sequence = new Sequence(10);
for(int i=0;i<10;i++){
sequence.add(Integer.toString(i));
}
Selector selector = sequence.selector();
while (!selector.end()) {
System.out.print(selector.current()+" ");
selector.next();
}
}
}
Sequence类只是一个固定大小的数组,以类的形式包装了起来。要获得Sequence中的每一个对象,可以使用Selector接口。这是“迭代器(Iterator)”设计模式的一个例子。Selector允许你检查序列是否到末尾了(end()),访问当前对象(current()),以及移到序列中的下一个对象(next())。因为Selector是一个接口,所以别的类可以按它们自己的方式来实现这个接口,并且别的方法能以此接口为参数,来生成更加通用的代码。从上面代码可以看出,内部类对象访问了外部类的私有(private)字段,内部类可以访问外部类的方法和字段,就像自己拥有了它们似的,这带来了很大的方便。
使用 .this和 .new。如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this。这样产生的引用自动的具有正确的类型,这一点在编译期就会被知晓并接受检查,因此没有任何运行时开销。有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在new表达式中提供对其他外部类对象的引用,这是需要使用 .new语法。要想直接创建外部类的对象,不是使用外部类名字创建,而是必须使用外部类的对象来创建该内部类的对象。代码如下:
package innerclass;
public class Outer {
void f() {System.out.println("Outer.f()");};
public class Inner {
public Outer out() {
//如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this
return Outer.this;
}
}
//在外围类里建立一个方法,该方法返回一个指向内部类的引用
public Inner in() {
return new Inner();
}
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner1 = outer.in();
inner1.out().f();
//在new表达式中提供对其他外部类对象的引用,这是需要使用 .new语法
Outer.Inner inner2 = outer.new Inner();
inner2.out().f();
}
}
内部类与向上转型。当将内部类向上转型为其基类,尤其是转型为一个接口时,内部类就有了用武之地。这是因为此内部类--某个接口的实现,能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便的隐藏实现细节。如下面的例子所示,Contents和Destination是2个接口,PContent和PDestination实现了接口,它们是Parcel的内部类,private内部类可以限制客户端的访问。客户端取得接口的引用时,甚至无法找到它的确切类型,因此不能依赖于类型编码,也无法得知接口方法的实现细节。
package innerclass;
public class Parcel {
//private内部类,除了Parcel没有人能访问它
private class PContent implements Contents{
private int i = 11;
public int value(){
return i;
}
}
//protected内部类,除了Parcel和它的子类(以及同一个包中的类),没有人能访问它
protected class PDestination implements Destination{
private String label;
private PDestination(String whereTo){
label = whereTo;
}
public String readLabel(){
return label;
}
}
//暴露公共的方法,Parcel类的对象可以调用此方法获取内部类的实例,无需关注实现的细节
public Destination destination(String s){
return new PDestination(s);
}
public Contents contents(){
return new PContent();
}
}
除了在类里面定义内部类这一普通做法,我们也可以在方法或某个作用域里定义内部类。在方法的作用域内中定义内部类,称之为局部内部类,它的作用范围就是方法的作用域内,超出此作用域则无法使用。匿名内部类更进一步,它连类名都没有,你只有在实例初始化时变相的构造它的实例。匿名内部类既可以扩展类,也可以扩展接口,但每次只能是其中一个。匿名内部类的合理使用,会使代码变得更加简洁优美。
package innerclass.game;
//Game接口
interface Game {
boolean move();
}
//Game工厂接口,通过它获取Game
interface GameFactory {
Game getGame();
}
class Checkers implements Game{
private Checkers(){}
private int moves = 0;
private static final int MOVES = 3;
public boolean move(){
System.out.println("Checkers move " + moves);
return ++moves != MOVES;
}
//匿名内部类,扩展接口,public static变量,可通过类名直接调用
public static GameFactory factory = new GameFactory() {
//向上转型
public Game getGame() {
return new Checkers();
}
};
}
class Chess implements Game{
private Chess(){}
private int moves = 0;
private static final int MOVES = 4;
public boolean move(){
System.out.println("Chess move " + moves);
return ++moves != MOVES;
}
//匿名内部类,扩展接口,静态public变量,可通过类名直接调用
public static GameFactory factory = new GameFactory() {
public Game getGame() {
return new Chess();
}
};
}
public class Games{
public static void playGame(GameFactory factory){
Game s = factory.getGame();
while (s.move());
}
//main方法测试
public static void main(String[] args) {
playGame(Checkers.factory);
playGame(Chess.factory);
}
}
运行结果:
Checkers move 0
Checkers move 1
Checkers move 2
Chess move 0
Chess move 1
Chess move 2
Chess move 3
如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static,这通常称为嵌套类。普通的内部类对象隐式地保存了一个引用,指向创建它的外部类对象。然而,嵌套类是static的,意味着可以直接使用外部类类名去调用静态方法创建内部类,而无需创建外部类对象。由于静态方法无法调用非静态属性和方法,所以不能从嵌套类的对象访问中访问非静态的外围类对象。普通内部类的字段和方法,只能放在类的外部层次上,所以不能有static数据和字段,也不能包含嵌套类,但是嵌套类都可以。
一般来说,内部类继承某个类或实现某个接口,内部类的代码操作创建它的外围类的对象,所以可以认为内部类提供了某种进入其外围类的窗口。每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类允许继承多个非接口类型(类或抽象类),使得多重继承的解决方案变得完整。如下:
package innerclass;
class D{}
abstract class E{}
//Z继承了D
class Z extends D{
E makeE() {//匿名内部类,相当于Z继承了E
return new E() {};
}
}
public class MultiImplementation {
static void takeD(D d) {}
static void takeE(E e) {}
public static void main(String[] args) {
Z z = new Z();
takeD(z);
takeE(z.makeE());
}
}
在代码块里创建内部类,典型的方式是在一个方法体里面创建。局部内部类不能有访问说明符,因为它不是外围类的一部分,但是它可以访问外围类的常量和所有成员。对于匿名内部类,只能用于实例初始化。