內部類


可以將一個類的定義放在另一個類的定義內部,這就是內部類。

創建內部類

創建內部類的方式就如同你想的一樣——把類的定義置於外圍類的裏面。

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編程思想(第四版)》
發佈了36 篇原創文章 · 獲贊 8 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章