【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對象信息。這些類文件的命名有嚴格的規則:外圍類的名字,加上"$",再加上內部類的名字。
如果內部類是匿名的,編譯器會簡單的產生一個數字作爲其標識符。如果內部類是嵌套在別的內部類之中,只需直接將它們的名字加在其外圍類標識符與"$"的後面。





發佈了80 篇原創文章 · 獲贊 0 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章