一篇文章讓你徹底瞭解Java內部類
內容整理自《Thinking in Java》(第四版) 第10章 PDF下載地址
什麼是內部類?
將一個類的定義,放在另一個類的定義內部,那這個類,就是內部類
為什麼需要內部類?
一般來說,內部類繼承自某個類或實現某個介面,內部類的程式碼操作建立其的外圍類的物件。所以你可以認為內部類提供了某種進入其外圍類的視窗。
內部類的優雅之處:
每個內部類都能獨立的繼承一個(介面的)實現,無論外部類是否已經繼承了一個(介面的)實現,對內部類都沒有影響。
內部類主要有以下幾類:
注意:
- 定義了成員內部類後,必須使用外部類物件來建立內部類物件,而不能直接去 new 一個內部類物件,
即:內部類 物件名 = 外部類物件.new 內部類( );
- 外部類是不能直接使用內部類的成員和方法滴,可先建立內部類的物件,然後通過內部類的物件來訪問其成員變數和方法。
- 可先建立內部類的物件,然後通過內部類的物件來訪問其成員變數和方法。HelloWorld.this.name
A:成員內部類
作為外部類的一個成員存在,與外部類的屬性、方法並列。
public class Outer {
private static int i = 1;
private int j = 10;
private int k = 20;
public static void outerF1() {
}
/**
* 外部類的靜態方法訪問成員內部類,與在外部類外部訪問成員內部類一樣
*/
public static void outerF4() {
//step1 建立外部類物件
Outer out = new Outer();
//step2 根據外部類物件建立內部類物件
Inner inner = out.new Inner();
//step3 訪問內部類的方法
inner.innerF1();
}
public static void main(String[] args) {
/*
* outerF4();該語句的輸出結果和下面三條語句的輸出結果一樣
*如果要直接建立內部類的物件,不能想當然地認為只需加上外圍類Outer的名字,
*就可以按照通常的樣子生成內部類的物件,而是必須使用此外圍類的一個物件來
*建立其內部類的一個物件:
*Outer.Inner outin = out.new Inner()
*因此,除非你已經有了外圍類的一個物件,否則不可能生成內部類的物件。因為此
*內部類的物件會悄悄地連結到建立它的外圍類的物件。如果你用的是靜態的內部類,
*那就不需要對其外圍類物件的引用。
*/
Outer out = new Outer();
Outer.Inner outin = out.new Inner();
outin.innerF1();
}
public void outerF2() {
}
/**
* 外部類的非靜態方法訪問成員內部類
*/
public void outerF3() {
Inner inner = new Inner();
inner.innerF1();
}
/**
* 成員內部類中,不能定義靜態成員
* 成員內部類中,可以訪問外部類的所有成員
*/
class Inner {
// static int innerI = 100;內部類中不允許定義靜態變數
// 內部類和外部類的例項變數可以共存
int j = 100;
int innerI = 1;
void innerF1() {
System.out.println(i);
//在內部類中訪問內部類自己的變數直接用變數名
System.out.println(j);
//在內部類中訪問內部類自己的變數也可以用this.變數名
System.out.println(this.j);
//在內部類中訪問外部類中與內部類同名的例項變數用外部類名.this.變數名
System.out.println(Outer.this.j);
//如果內部類中沒有與外部類同名的變數,則可以直接用變數名訪問外部類變數
System.out.println(k);
outerF1();
outerF2();
}
}
}
注意:內部類是一個編譯時的概念,一旦編譯成功,就會成為完全不同的兩類。
對於一個名為outer的外部類和其內部定義的名為inner的內部類。編譯完成後出現outer.class和outer$inner.class兩類。
##
B:區域性內部類在方法中定義的內部類稱為區域性內部類。與區域性變數類似,區域性內部類不能有訪問說明符,因為它不是外圍類的一部分,但是它可以訪問當前程式碼塊內的常量,和此外圍類所有的成員。
public class Outer {
private int s = 100;
private int outI = 1;
public static void main(String[] args) {
// 訪問區域性內部類必須先有外部類物件
Outer out = new Outer();
out.f(3);
}
public void f(final int k) {
final int s = 200;
int i = 1;
final int j = 10;
/**
* 定義在方法內部
*/
class Inner {
// 可以定義與外部類同名的變數
int s = 300;
int innerI = 100;
// static int m = 20; 不可以定義靜態變數
Inner(int k) {
innerF(k);
}
void innerF(int k) {
// java如果內部類沒有與外部類同名的變數,在內部類中可以直接訪問外部類的例項變數
System.out.println(outI);
// 可以訪問外部類的區域性變數(即方法內的變數),但是變數必須是final的
System.out.println(j);
//System.out.println(i);
// 如果內部類中有與外部類同名的變數,直接用變數名訪問的是內部類的變數
System.out.println(s);
// 用this.變數名訪問的也是內部類變數
System.out.println(this.s);
// 用外部類名.this.內部類變數名訪問的是外部類變數
System.out.println(Outer.this.s);
}
}
new Inner(k);
}
}
C:靜態內部類(巢狀類):
注意:前兩種內部類與變數類似,所以可以對照參考變數
如果你不需要內部類物件與其外圍類物件之間有聯絡,那你可以將內部類宣告為static。這通常稱為巢狀類(nested class)。想要理解static應用於內部類時的含義,你就必須記住,普通的內部類物件隱含地儲存了一個引用,指向建立它的外圍類物件。然而,當內部類是static的時,就不是這樣了。巢狀類意味著:
- 要建立巢狀類的物件,並不需要其外圍類的物件。
- 不能從巢狀類的物件中訪問非靜態的外圍類物件。
單例模式:由於靜態內部類的載入機制,決定了他可以使用來處理單例模式,而且效能客觀單例模式相關內容>>點我
public class Outer {
private static int i = 1;
private int j = 10;
public static void outerF1() {
}
public static void main(String[] args) {
new Outer().outerF3();
}
public void outerF2() {
}
public void outerF3() {
// 外部類訪問內部類的靜態成員:內部類.靜態成員
System.out.println(Inner.inner_i);
Inner.innerF1();
// 外部類訪問內部類的非靜態成員:例項化內部類即可
Inner inner = new Inner();
inner.innerF2();
}
/**
* 靜態內部類可以用public,protected,private修飾
* 靜態內部類中可以定義靜態或者非靜態的成員
*/
static class Inner {
static int inner_i = 100;
int innerJ = 200;
static void innerF1() {
// 靜態內部類只能訪問外部類的靜態成員(包括靜態變數和靜態方法)
System.out.println("Outer.i" + i);
outerF1();
}
void innerF2() {
// 靜態內部類不能訪問外部類的非靜態成員(包括非靜態變數和非靜態方法)
// System.out.println("Outer.i"+j);
// outerF2();
}
}
}
靜態內部類和成員內部類的區別
生成一個靜態內部類不需要外部類成員
靜態內部類的物件可以直接生成:
Outer.Inner in = new Outer.Inner();
而不需要通過生成外部類物件來生成。這樣實際上使靜態內部類成為了一個頂級類
(正常情況下,你不能在介面內部放置任何程式碼,但巢狀類可以作為介面的一部分,因為它是static 的。只是將巢狀類置於介面的名稱空間內,這並不違反介面的規則)
D:匿名內部類(from thinking in java 3th)
匿名內部類就是沒有名字的內部類。
什麼情況下需要使用匿名內部類?
如果滿足下面的一些條件,使用匿名內部類是比較合適的:
- 只用到類的一個例項。
- 類在定義後馬上用到。
- 類非常小(SUN推薦是在4行程式碼以下)
- 給類命名並不會導致你的程式碼更容易被理解。
在使用匿名內部類時,要記住以下幾個原則:
- 匿名內部類一般不能有構造方法。
- 匿名內部類不能定義任何靜態成員、方法和類。
- 匿名內部類不能是public,protected,private,static。
- 只能建立匿名內部類的一個例項。
- 一個匿名內部類一定是在new的後面,用其隱含實現一個介面或實現一個類。
- 因匿名內部類為區域性內部類,所以區域性內部類的所有限制都對其生效。
下面的例子看起來有點奇怪:
// 在方法中返回一個匿名內部類
public class Parcel6 {
public static void main(String[] args) {
Parcel6 p = new Parcel6();
Contents c = p.cont();
}
public Contents cont() {
return new Contents() {
private int i = 11;
public int value() {
return i;
}
}; // 在這裡需要一個分號
}
}
cont()
方法將下面兩個動作合併在一起:返回值的生成,與表示這個返回值的類的定義!
進一步說,這個類是匿名的,它沒有名字。更糟的是,看起來是你正要建立一個Contents物件:
return new Contents()
但是,在到達語句結束的分號之前,你卻說:“等一等,我想在這裡插入一個類的定義”:
return new Contents() {
private int i = 11;
public int value() {
return i;
}
};
這種奇怪的語法指的是:“建立一個繼承自Contents的匿名類的物件。”通過new 表示式返回的引用被自動向上轉型為對Contents的引用。匿名內部類的語法是下面例子的簡略形式:
class MyContents implements Contents {
private int i = 11;
public int value() {
return i;
}
}
return new MyContents();
在這個匿名內部類中,使用了預設的構造器來生成Contents。下面的程式碼展示的是,如果你的基類需要一個有引數的構造器,應該怎麼辦:
public class Parcel7 {
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Wrapping w = p.wrap(10);
}
public Wrapping wrap(int x) {
// Base constructor call:
// Pass constructor argument.
return new Wrapping(x) {
public int value() {
return super.value() * 47;
}
}; // Semicolon required
}
}
只需簡單地傳遞合適的引數給基類的構造器即可,這裡是將x 傳進new Wrapping(x)。在匿名內部類末尾的分號,並不是用來標記此內部類結束(C++中是那樣)。實際上,它標記的是表示式的結束,只不過這個表示式正巧包含了內部類罷了。因此,這與別的地方使用的分號是一致的。
如果在匿名類中定義成員變數,你同樣能夠對其執行初始化操作:
public class Parcel8 {
public static void main(String[] args) {
Parcel8 p = new Parcel8();
Destination d = p.dest("Tanzania");
}
// Argument must be final to use inside
// anonymous inner class:
public Destination dest(final String dest) {
return new Destination() {
private String label = dest;
public String readLabel() {
return label;
}
};
}
}
如果你有一個匿名內部類,它要使用一個在它的外部定義的物件,編譯器會要求其引數引用是final 型的,就像dest()
中的引數。如果你忘記了,會得到一個編譯期錯誤資訊。如果只是簡單地給一個成員變數賦值,那麼此例中的方法就可以了。但是,如果你想做一些類似構造器的行為,該怎麼辦呢?在匿名類中不可能有已命名的構造器(因為它根本沒名字!),但通過例項初始化,你就能夠達到為匿名內部類“製作”一個構造器的效果。像這樣做:
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 anonymous f()");
}
};
}
public static void main(String[] args) {
Base base = getBase(47);
base.f();
}
}
在此例中,不要求變數i 一定是final 的。因為i 被傳遞給匿名類的基類的構造器,它並不會在匿名類內部被直接使用。下例是帶例項初始化的“parcel”形式。注意dest()的引數必須是final,因為它們是在匿名類內被使用的。
public class Parcel9 {
public Destinationdest(final String dest, final float price) {
return new Destination() {
private int cost;
private String label = dest;
// Instance initialization for each object:
{
cost = Math.round(price);
if (cost > 100)
System.out.println("Over budget!");
}
public String readLabel() {
return label;
}
};
}
public static void main(String[] args) {
Parcel9 p = new Parcel9();
Destination d = p.dest("Tanzania", 101.395F);
}
}
在例項初始化的部分,你可以看到有一段程式碼,那原本是不能作為成員變數初始化的一部分而執行的(就是if 語句)。所以對於匿名類而言,例項初始化的實際效果就是構造器。當然它受到了限制:你不能過載例項初始化,所以你只能有一個構造器。
從多層巢狀類中訪問外部
一個內部類被巢狀多少層並不重要,它能透明地訪問所有它所嵌入的外圍類的所有成員,如下所示:
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”語法能產生正確的作用域,所以你不必在呼叫構造器時限定類名。
###內部類的過載問題
如果你建立了一個內部類,然後繼承其外圍類並重新定義此內部類時,會發生什麼呢?也就是說,內部類可以被過載嗎?這看起來似乎是個很有用的點子,但是“過載”內部類就好像它是外圍類的一個方法,其實並不起什麼作用:
class Egg {
private Yolk y;
public Egg() {
System.out.println("New Egg()");
y = new Yolk();
}
protected class Yolk {
public Yolk() {
System.out.println("Egg.Yolk()");
}
}
}
public class BigEgg extends Egg {
public static void main(String[] args) {
new BigEgg();
}
public class Yolk {
public Yolk() {
System.out.println("BigEgg.Yolk()");
}
}
}
輸出結果為:
New Egg()
Egg.Yolk()
預設的構造器是編譯器自動生成的,這裡是呼叫基類的預設構造器。你可能認為既然建立了BigEgg 的物件,那麼所使用的應該是被“過載”過的Yolk,但你可以從輸出中看到實際情況並不是這樣的。
這個例子說明,當你繼承了某個外圍類的時候,內部類並沒有發生什麼特別神奇的變化。這兩個內部類是完全獨立的兩個實體,各自在自己的名稱空間內。當然,明確地繼承某個內部類也是可以的:
class Egg2 {
private Yolk y = new Yolk();
public Egg2() {
System.out.println("New Egg2()");
}
public void insertYolk(Yolk yy) {
y = yy;
}
public void g() {
y.f();
}
protected class Yolk {
public Yolk() {
System.out.println("Egg2.Yolk()");
}
public void f() {
System.out.println("Egg2.Yolk.f()");
}
}
}
public class BigEgg2 extends Egg2 {
public BigEgg2() {
insertYolk(new Yolk());
}
public static void main(String[] args) {
Egg2 e2 = new BigEgg2();
e2.g();
}
public class Yolk extends Egg2.Yolk {
public Yolk() {
System.out.println("BigEgg2.Yolk()");
}
public void f() {
System.out.println("BigEgg2.Yolk.f()");
}
}
}
輸出結果為:
Egg2.Yolk()
New Egg2()
Egg2.Yolk(