【Java编程思想】(6)内部类

1. 创建内部类

更典型的情况是,外部类将有一个方法,该方法返回一个指向内部类的引用。

public class Parcel2 {
	class Contents {
		private int i = 11;
		public int value() {
			return i;
		}
	}

	class Destination {
		private String label;
		Destination(String whereTo) {
			label = whereTo;
		}
		String readLabel() {
			return label;
		}
	}

	public Destination to(String s) {
		return new Destination(s);
	}

	public Contents contents() {
		return new Contents();
	}

	public void ship(String dest) {
		Contents c = contents();
		Destination d = to(dest);
		System.out.println(d.readLabel());
	}

	public static void main(String[] args) {
		Parcel2 p = new Parcel2();
		p.ship("Tasmania");
		Parcel2 q = new Parcel2();
		// Defining references to inner classes:
		Parcel2.Contents c = q.contents();
		Parcel2.Destination d = q.to("Borneo");
	}
}
如果想从外部类的非静态方法之外的任意位置创建某个内部类的对象,那么必须具体地指明这个对象的类型:OuterClassName.InnerClassName。


2. 链接到外部类

当生成一个内部类的对象时,此对象与制造它的外围对象(enclosing object)之间就有了一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类的所有元素的访问权。

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.println(selector.current() + " ");
			selector.next();
		}
	}
}
当某个外围类的对象创建了一个内部类对象时,此内部类对象必定会秘密的捕获一个指向那个外围类对象的引用。然后,在你访问此外围类的成员时,就是用那个引用来选择外围类的成员。


3. 使用this与new

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

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

public class DotNew {
	public class Inner{}
	public static void main(String[] args) {
		DotNew dn = new DotNew();
		DotNew.Inner dni = dn.new Inner();
	}
}

要想直接创建内部类的对象,你不能按照你想象的方式,去引用外部类的名字DotNew,而是必须使用外部类的对象来创建该内部类对象。这也解决了内部类名字作用域的问题,因此你不必声明(实际上你不能声明)dn.new DotNew.Inner()。

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


4. 内部类与向上转型

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


5. 在方法和作用域内的内部类

可以在一个方法里面或者在任意的作用域内定义内部类,这么做有两个理由:

(1). 实现了某类型的接口,于是可以创建并返回对其的引用。

(2). 解决一个复杂的问题,想创建一个类来辅助你的解决方案,但是又不希望这个类是公共可用的。

内部类的使用情况:

(1). 定义在方法中的类

public class Parcel5 {

	public Destination destination(String s) {
		class PDestination implements Destination {
			private String label;

			private PDestination(String whereTo) {
				label = whereTo;
			}

			public String readLabel() {
				return label;
			}
		}
		return new PDestination(s);
	}

	public static void main(String[] args) {
		Parcel5 p = new Parcel5();
		Destination d = p.destination("Tasmania");
	}
}
(2). 定义在作用域内的类,此作用域在方法的内部。

public class Parcel6 {

	private void internalTracking(boolean b) {
		if (b) {
			class TrackingSlip {
				private String id;
				TrackingSlip(String s) {
					id = s;
				}
				String getSlip() {
					return id;
				}
				TrackingSlip ts = new TrackingSlip("slip");
				String s = ts.getSlip();
			}
			// Can't use it here! Out of scope:
			// ! TrackingSlip ts = new TrackingSlip("x");
		}
	}
	public void track() {
		internalTracking(true);
	}
	public static void main(String[] args) {
		Parcel6 p = new Parcel6();
		p.track();
	}
}
(3). 实现了接口的匿名类。

public class Parcel7 {
	public Contents contents() {
		return new Contents() {
			private int i = 11;
			public int value() {
				return i;
			}
		};
	}

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

}
(4). 匿名类,它扩展了有非默认构造器的类

public class Parcel8 {

	public Wrapping wrapping(int x) {
		// Base constructor call:
		return new Wrapping(x) { // pass constructor arguments
			public int value() {
				return super.value() * 47;
			}
		}; // Semicolon required
	}

	public static void main(String[] args) {
		Parcel8 p = new Parcel8();
		Wrapping w = p.wrapping(10);
	}
}
(5). 匿名类,它执行字段初始化。

public class Parcel9 {

	public Destination destination(final String dest) {
		return new Destination() {
			private String label = dest;
			public String readLabel() {
				return label;
			}
		};
	}

	public static void main(String[] args) {
		Parcel9 p = new Parcel9();
		Destination d = p.destination("Tasmania");
	}
}
(6). 匿名类,它通过实例初始化实现构造(匿名类不可能有构造器)。

abstract class Base {
	public Base(int i) {
		System.out.println("Base constructor. i = " + i);
	}

	public abstract void f();
}

public class AnonymousConstructor {

	public static Base getBase(int i ) {
		return new Base(i) {
			{
				System.out.println("Inside instance initializer");
			}
			public void f() {
				System.out.println("In anomymous f()");
			}
		};
	}
	
	public static void main(String[] args) {
		Base base = getBase(47);
		base.f();
	}
}

传递到匿名内部类中并使用的参数必须是final的。

在实例初始化操作的内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是if语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制——你不能重载实例初始化方法,所以你仅有一个这样的构造器。

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

优先使用类而不是接口。如果你的设计中需要某个接口,你必须了解它。否则,不到迫不得已,不要将其放到你的设计中。


6. 嵌套类

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

(1). 要创建嵌套类的对象,并不需要其外围类的对象。

(2). 不能从嵌套类的对象中访问非静态的外围类对象。

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

在一个普通的(非static)内部类中,通过一个特殊的this引用可以链接到其外围类对象。嵌套类就没有这个特殊的this引用,这使得它类似于一个static方法。


7. 接口内部的类

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

public interface ClassInInterface {
	void howdy();

	class Test implements ClassInInterface {
		public void howdy() {
			System.out.println("Howdy!");
		}

		public static void main(String[] args) {
			new Test().howdy();
		}
	}
}
如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便。

一个内部类被嵌套多少层并不重要——它能透明的访问所有它所嵌入的外围类的所有成员:

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. 为什么需要内部类

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

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

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

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

如果不需要解决“多重继承”的问题,那么自然可以用别的方式编码,而不需要使用内部类。单如果使用内部类,还可以获得其他一些特性:

(1). 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。

(2). 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。

(3). 创建内部类对象的时刻并不依赖于外围类对象的创建

(4). 内部类并没有令人迷惑的"is-a"关系;它就是一个独立的实体。


9. 闭包与回调

闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面相对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括private成员。
Java最引人争议的问题之一就是,人们认为Java应该包含某种类似指针的机制,以允许回到(callback)。通过回调,对象能够携带一些信息,这些信息允许它在稍后的某个时刻调用初始的对象。如果回调是通过指针实现的,那么就只能寄希望于程序员不会误用该指针。然而,Java更小心仔细,所以没有在语言中包括指针。
通过内部类提供闭包功能是优良的解决方案,它比指针更灵活、更安全:
interface Incrementable {
	void increment();
}

// very simple to just implement the interface:
class Callee1 implements Incrementable {
	private int i = 0;

	public void increment() {
		i++;
		System.out.println(i);
	}
}

class MyIncrement {
	public void increment() {
		System.out.println("Other operation");
	}

	static void f(MyIncrement mi) {
		mi.increment();
	}
}

// If your class must implement increment() in some other way, you must use an
// inner class:
class Callee2 extends MyIncrement {
	private int i = 0;

	public void increment() {
		super.increment();
		i++;
		System.out.println(i);
	}

	private class Closure implements Incrementable {
		public void increment() {
			// Specify outer-class method, otherwise you'd get an infinite
			// recursion:
			Callee2.this.increment();
		}
	}

	Incrementable getCalIncrementable() {
		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) {
		Callee1 c1 = new Callee1();
		Callee2 c2 = new Callee2();
		MyIncrement.f(c2);
		Caller caller1 = new Caller(c1);
		Caller caller2 = new Caller(c2.getCalIncrementable());
		caller1.go();
		caller1.go();
		caller2.go();
		caller2.go();
	}
}
回调的价值在于它的灵活性——可以运行时动态的决定需要调用什么方法。

10. 内部类与控制框架

应用程序框架(application framework)就是被设计用以解决某类特定问题的一个类或一组类。要运用某个应用程序框架,通常是继承一个或多个类,并覆盖某些方法。在覆盖后的方法中,编写代码定制应用程序框架提供的通用解决方案,以解决特定问题(这是设计模式中模板方法的一个例子)。模板方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法的动作。设计模式总是将变化的事物与保持不变的事物分开,在这个模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物。

控制框架是一类特殊的应用程序框架,它用来解决响应事件的需求。主要用来响应事件的系统被称作事件驱动系统。应用程序设计中常见的问题之一是图形用户接口(GUI),它几乎完全是事件驱动的系统。


11. 内部类的继承

class WithInner {
	class Inner {
	}
}

public class InheritInner extends WithInner.Inner {

	public InheritInner(WithInner wi) {
		wi.super();
	}

	public static void main(String[] args) {
		WithInner wi = new WithInner();
		InheritInner ii = new InheritInner(wi);
	}
}
可以看到,InheritInner只继承自内部类,而不是外围类。但是当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外围类对象的引用。此外,必须在构造器内使用如下语法:
enclosingClassReference.super();
这样才提供了必要的引用,然后程序才能编译通过。

12. 内部类可以被覆盖吗

class Egg {
	private Yolk y;

	protected class Yolk {
		public Yolk() {
			System.out.print("Egg.Yolk()");
		}
	}

	public Egg() {
		System.out.print("New Egg()");
		y = new Yolk();
	}
}

public class BigEgg {
	public class Yolk {
		public Yolk() {
			System.out.print("BigEgg.Yolk()");
		}
	}

	public static void main(String[] args) {
		new BigEgg();
	}
}
默认的构造器是编译器自动生成的,这里是调用基类的默认构造器。你可能认为既然创建了BigEgg的对象,那么所使用的应该是“覆盖后”的Yolk版本,但从输出中可以看到实际情况并不是这样的。
这个例子说明,当继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内。
明确的继承某个内部类也是可以的:
class Egg2 {
	protected class Yolk {
		public Yolk() {
			System.out.println("Egg2.Yolk()");
		}

		public void f() {
			System.out.println("Egg2.Yolk.f()");
		}
	}

	private Yolk y = new Yolk();

	public Egg2() {
		System.out.println("New Egg2()");
	}

	public void insertYolk(Yolk yy) {
		y = yy;
	}

	public void g() {
		y.f();
	}
}

public class BigEgg2 extends Egg2 {
	public class Yolk extends Egg2.Yolk {
		public Yolk() {
			System.out.println("BigEgg2.Yolk()");
		}

		public void f() {
			System.out.println("BigEgg2.Yolk.f()");
		}
	}

	public BigEgg2() {
		insertYolk(new Yolk());
	}

	public static void main(String[] args) {
		Egg2 e2 = new BigEgg2();
		e2.g();
	}
}
现在BigEgg2.Yolk通过extends Egg2.Yolk明确地继承了此内部类,并且覆盖了其中的方法。insertYolk()方法允许BigEgg2将它自己的Yolk对象向上转型为Egg2中的引用y。所以当g()调用y.f()时,覆盖后的新版的f()被执行。第二次调用Egg2.Yolk(),结果是BigEgg2.Yolk的构造器调用了其基类的构造器。可以看到在调用g()的时候,新版的f()被调用了。

13. 局部内部类

可以在代码块里创建内部类,典型的方式是在一个方法体的里面创建。局部内部类不能有访问说明符,因为它不是外围类的一部分;但是可以访问当前代码块内的常量,以及此外围类的所有成员。

interface Counter {
	int next();
}

public class LocalInnerClass {
	private int count = 0;

	Counter getCounter(final String name) {
		// A local inner class:
		class LocalCounter implements Counter {
			public LocalCounter() {
				System.out.println("LocalCounter()");
			}

			public int next() {
				System.out.println(name);
				return count++;
			}
		}
		return new LocalCounter();
	}

	// The same thing with an anonymous inner class:
	Counter getCounter2(final String name) {
		return new Counter() {
			{
				System.out.println("Counter()");
			}

			public int next() {
				System.out.println(name);
				return count++;
			}
		};
	}

	public static void main(String[] args) {
		LocalInnerClass lic = new LocalInnerClass();
		Counter c1 = lic.getCounter("Local inner"), c2 = lic
				.getCounter2("Anomymous inner");
		for (int i = 0; i < 5; i++) {
			System.out.println(c1.next());
		}
		for (int i = 0; i < 5; i++) {
			System.out.println(c2.next());
		}
	}
}

Counter返回的是序列中的下一个值。我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力。既然局部内部类的名字在方法外是不可见的,那为什么我们让然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能用于实例初始化。

所以使用局部内部类而不是用匿名内部类的另一个理由就是,需要不止一个该内部类的对象。


14. 内部类标识符

内部类在编译的时候也会产生一个.class文件以包含它们的class对象信息。这些类文件的命名有严格的规则:外围类的名字,加上"$",再加上内部类的名字。
如果内部类是匿名的,编译器会简单的产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外围类标识符与"$"的后面。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值