内部类


可以将一个类的定义放在另一个类的定义内部,这就是内部类。

创建内部类

创建内部类的方式就如同你想的一样——把类的定义置于外围类的里面。

public class A{
	class B{
		private int i = 1;
		public int getI(){
			return i;
		}
	}
	public void f(){
		B b = new B();
		System.out.println(b.getI());
	}
	public static void main(String[] args) {
		A a = new A();
		a.f();
	}
	//输出:1
}

当我们在f()方法里面使用内部类的时候,这与使用普通类没什么不同。内部类似乎还只是一种名字隐藏和组织代码的模式。这些是很有用,但还不是最引人注目的,它还有其他的用途。当生成一个内部类的对象时,此对象与制造它的外围对象(enclosing object)之间就有了一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外内部类还拥有其外围类的所有元素的访问权。下面的例子说明了这点:

public class A{
	int t = 2;
	class B{
		public int getAValue(){
			return t;
		}
	}
	public B getB(){
		return new B();
	}
	public static void main(String[] args) {
		A a = new A();
		B b = a.getB();
		System.out.println(b.getAValue());
	}
	//输出:2
}

所以内部类自动拥有对其外围类所有成员的访问权。这是如何做到的呢?当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外围类对象的引用。然后在你访问此外围类的成员时,就是用那个引用来选择外围类的成员。幸运的是编译器会帮你处理所有的细节,但你现在可以看到:内部类的对象只能在与其外为类的对象相关联的情况下才能被创建(在内部类是非static类时)。构建内部类对象时,需要一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。不过绝大多数时候这都不需要程序员操心。

使用.this与.new

如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并接受到检査,因此没有任何运行时开销。下面的示例展示了如何使用.this:

public class DotThis{
	public class Inner{
		public DotThis getDotThis(){
			return DotThis.this;
		}
	}
	public Inner getInner(){
		return new Inner();
	}
	public static void main(String[] args) {
		DotThis d = new DotThis();
		Inner i = d.getInner();
		System.out.println(d == i.getDotThis());
	}
	//输出:true
}

有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在new表达式中提供对其他外部类对象的引用,这是需要使用.new语法,就像下面这样:

public class Outer{
	public class Inner{}
	public static void main(String[] args) {
		Outer o = new Outer();
		Inner i = o.new Inner();
	}
}

在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到创建它的外部类对象上。但是如果你创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用 。

public class Outer {
	public static class Inner{
		public Inner() {
			System.out.println("Inner init..");
		}
	}
	public static void main(String[] args) {
		Inner i = new Inner();
	}
	//输出:Inner init..
}

内部类与向上转型

当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类——某个接口的实现——能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。当取得了一个指向基类或接口的引用时,甚至可能无法找出它确切的类型,看下面的例子:

public class Parcel4 {
	public interface Contents{}
	public interface Destination{}
	
	private class PContents implements Contents{}
	
	protected class PDestination implements Destination{
		private PDestination(){}
	}
	
	public PContents contents(){
		return new PContents();
	}
	
	public PDestination destination(){
		return new PDestination();
	}
	
	public static void main(String[] args) {
		Parcel4 p = new Parcel4();
		Contents c = p.contents();
		Destination d = p.destination();
	}
}

Parcel4中增加了一些新东西:内部类PContents 是private,所以除了Parcel4,没有人能访问它。 PDestination是protected,所以只有Parcel4及其子类、还有与Parcel4同一个包中的类(因为protected也给予了包访问权)能访问PDestination,其他类都不能访问PDestination。这意味着,如果客户端程序员想了解或访问这些成员,那是要受到限制的。实际上甚至不能向下转型成Private内部类(或protected内部类,除非是继承自它的子类),因为不能访问其名字。于是,private内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给Java编译器提供了生成更高效代码的机会。

方法和作用域内的内部类

到目前为止,所看到的只是内部类的典型用造。通常,如果所读、写的代码包含了内部类,那么它们都是“平凡的”内部类,筒单并且容易理解。然而,内部类的语法覆盖了大量其他的更加难以理解的技术。例如,可以在一个方法里面或者在任意的作用域内定义内部类。这么做有两个理由:

  1. 你实现了某类型的接口,于是可以创建并返回对其的引用。
  2. 你要解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。

第一个例子展示了在方法的作用域内,创建一个完整的类。这被称作局部内部类,我们继续沿用上面的代码:

public class Parcel5 {
	public Destination destination(){
		class innerDestination implements Destination{
			//此处的实现可以有效的隐藏
		}
		return new innerDestination();
	}
	
	public static void main(String[] args) {
		Parcel5 p = new Parcel5();
		p.destination();
	}
}

下面的例子展示了如何在任意的作用域内嵌入一个内部类:

public class Parcel6 {
	
	private Destination f(boolean b){
		if(b){
			class innerDestination implements Destination{
				//此处的实现可以有效的隐藏
			}
			return new innerDestination();
		}else{
			class innerDestination implements Destination{
				//此处另一种实现也可以有效的隐藏
			}
			return new innerDestination();
		}
	}
	
	public static void main(String[] args) {
		Parcel6 p = new Parcel6();
		p.f(false);
	}
}

局部内部类不能有访问说明符,因为它们不是外围类的一部分。但是它可以访问当前代码块内的常量,以及此外围类的所有成员。

匿名内部类

我们继续沿用上面的代码,看下面这个例子:

public class Parcel7 {
	public Contents contents(){
		return new Contents() {};
	}
	
	public static void main(String[] args) {
		Parcel7 p = new Parcel7();
		Contents c = p.contents();
	}
}

这个类是匿名的,他没有名字。更糟的是,看起来似乎是你正要创建一个Contents 对象。但是然后(到达语句结束的分号之前)你却说:“等一等,我想在这里插入一个类的定义。”这种奇径的语法指的是:“创建一个继承自Contents的匿名类的对象。通过new表达式返回的引用被自动向上转型为对Contents的引用。

在匿名内部类末尾的分号、并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此这与别的地方使用的分号是一致的。

匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现了接口,也只能实现一个接口。

嵌套类

如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static。这通常称为嵌套类。想要理解static应用内部类时的含义,就必须记佳,普通的内部类对象隐式地保存了一个引用,指向创建它的外围类对象。然而当内部类是static的时候就不是这样了。嵌套类意味着:

  1. 要创建嵌套类的对象,并不需要其外围类的对象。
  2. 不能从嵌套类的对象中访问非静态的外围类对象。

嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部装不能有static数据和static字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西。

public class Parcel8 {
	public class Parcelson{
		static int i = 1;//编译报错
	}

	static class Parcelson2{
		static int i= 1;
	};
}

接口内部的类
正常情况下,不能在接口内部放置任何代码,但嵌套类可以作为接口的一部分。你放到接口中的任何类都自动地是Public和static的。因为类是static的,只是将嵌套类置于接口的命名空问内,这并不违反接口的规则。你甚至可以在内部类中实现其外围接口,就像下面这样:

public interface Parcel9 {
	void f();
	class Test implements Parcel9{
		@Override
		public void f() {
			System.out.println("Test.f()");
		}
		public static void main(String[] args) {
			new Test().f();
		}
	}
}

为什么需要内部类

至此,我们已经看到了许多描述内部类的语法和语义,但是这并不能回答“为什么需要内部类”这个问题。那么,Sun公司为什么会如此费心地增加这项基本的悟言特性呢?

一般说来,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口 。

内部类必须要回答的一个问题是:如果只是需要一个对接口的引用,为什么不通过外围类实现那个接口呢?答案是:“如果这能满足需求,那么就应该这样做。”那么内部类实现一个接口与外围类实现这个接口有什么区别呢?答案是:后者不是总能享用到接口带来的方便,有时需要用到接口的实现。所以使用内部类最吸引人的原因是:

每个内部类都能独立地继承地继承一个(接口的)实观,所以无论外围类是否已经继承了某个(接口的)实观,对于内部类都没有影响。

如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问題就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说内部类允许继承多个非接口类型(类或抽象类)。

内部类的继承

因为内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类的时候,事情会变得有点复杂。问题在于,那个指向外围类对象的“秘密的”引用必须被初始化,而在导出类中不再存在可连接的默认对象。要解决这个问题,必须使用特殊的语法来明确说清它们之间的关联:

class WithInner{
	class Inner{}
}

public class InherInner extends WithInner.Inner{
	public InherInner(WithInner w){
		w.super();
	}
	public static void main(String[] args) {
		WithInner w = new WithInner();
		InherInner i = new InherInner(w);
	}
}

可以看到InherInner只继承自内部类而不是外围类,但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用。此外必须在构造器内使用如下语法:

enclosingClassReference.super();

这样才提供了必要的引用,然后程序才能编译通过。

内部类标识符

由于每个类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个“meta-class”,叫做Class对象),你可能猜到了,内部类也必须生成一个.class文件以 包含它们的Class对象信息。这些类文件的命名有严格的规则:外围类的名字加上“$”,再加上内部类的名字。例如上例InherInner.java生成的.class文件包括:
InherInner.class
WithInner$Inner.class
WithInner.class
如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在別的内部类之中,只需直接将它们的名字加在其外围类标识符与“$”的后面。

虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况。因为这是Java的标准命名方式,所以产生的文件自动都是平台无关的。(注意,为了保证你的内部类能起作用,Java编译器会尽可能地转换它们。)


  1. 本文来源《Java编程思想(第四版)》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值