Java 內(nèi)部類
本節(jié)我們將介紹 Java 中的內(nèi)部類。通過本節(jié)的學習,我們將了解到什么是內(nèi)部類,內(nèi)部類的分類和作用。在內(nèi)部類的分類部分,我們將逐一學習各個類型的內(nèi)部類如何定義,如何實例化以及各自的特點,要注意區(qū)分不同類型內(nèi)部類的異同。有了這些基礎知識之后,我們也會結合示例介紹為什么需要內(nèi)部類。
1. 概念
在 Java 語言中,可以將一個類定義在另一個類里面或者一個方法里面,我們把這樣的類稱為內(nèi)部類。
與之對應的,包含內(nèi)部類的類被稱為外部類。請閱讀下面的代碼:
// 外部類 Car
public class Car {
// 內(nèi)部類 Engine
class Engine {
private String innerName = "發(fā)動機內(nèi)部類";
}
}
代碼中,Engine
就是內(nèi)部類,而 Car
就是外部類。
2. 分類
Java 中的內(nèi)部類可以分為 4 種:成員內(nèi)部類、靜態(tài)內(nèi)部類、方法內(nèi)部類和匿名內(nèi)部類。接下來我們按照分類一一介紹。
2.1 成員內(nèi)部類
2.1.1 定義
成員內(nèi)部類也稱為普通內(nèi)部類,它是最常見的內(nèi)部類??梢詫⑵淇醋魍獠款惖囊粋€成員。在成員內(nèi)部類中無法聲明靜態(tài)成員。
如下代碼中聲明了一個成員內(nèi)部類:
// 外部類 Car
public class Car {
// 內(nèi)部類 Engine
private class Engine {
private void run() {
System.out.println("發(fā)動機啟動了!");
}
}
}
我們在外部類 Car
的內(nèi)部定義了一個成員內(nèi)部類 Engine
,在 Engine
下面有一個 run()
方法,其功能是打印輸出一行字符串:“發(fā)動機啟動了!”。
另外,需要注意的是,與普通的 Java 類不同,含有內(nèi)部類的類被編譯器編譯后,會生成兩個獨立的字節(jié)碼文件:
Car$Engine.class
Car.class
內(nèi)部類 Engine
會另外生成一個字節(jié)碼文件,其文件名為:外部類類名 $ 內(nèi)部類類名.class。
2.1.2 實例化
內(nèi)部類在外部使用時,無法直接實例化,需要借助外部類才能完成實例化操作。關于成員內(nèi)部類的實例化,有 3 種方法:
- 我們可以通過
new 外部類().new 內(nèi)部類()
的方式獲取內(nèi)部類的實例對象:
// 外部類 Car
public class Car {
// 內(nèi)部類 Engine
private class Engine {
private void run() {
System.out.println("發(fā)動機啟動了!");
}
}
public static void main(String[] args) {
// 1.實例化外部類后緊接著實例化內(nèi)部類
Engine engine = new Car().new Engine();
// 2.調(diào)用內(nèi)部類的方法
engine.run();
}
}
運行結果:
發(fā)動機啟動了!
- 我們可通過先實例化外部類、再實例化內(nèi)部類的方法獲取內(nèi)部類的對象實例:
public static void main(String[] args) {
// 1.實例化外部類
Car car = new Car();
// 2.通過外部類實例對象再實例化內(nèi)部類
Engine engine = car.new Engine();
// 3.調(diào)用內(nèi)部類的方法
engine.run();
}
編譯執(zhí)行,成功調(diào)用了內(nèi)部類的 run () 方法:
$javac Car.java
java Car
發(fā)動機啟動了!
- 我們也可以在外部類中定義一個獲取內(nèi)部類的方法
getEngine()
,然后通過外部類的實例對象調(diào)用這個方法來獲取內(nèi)部類的實例:
// 外部類 Car
public class Car {
// 獲取內(nèi)部類實例的方法
public Engine getEngine() {
return new Engine();
}
// 內(nèi)部類 Engine
private class Engine {
private void run() {
System.out.println("發(fā)動機啟動了!");
}
}
public static void main(String[] args) {
// 1.實例化外部類
Car car = new Car();
// 2.調(diào)用實例方法getEngine(),獲取內(nèi)部類實例
Engine engine = car.getEngine();
// 3.調(diào)用內(nèi)部類的方法
engine.run();
}
}
運行結果:
發(fā)動機啟動了!
這種設計在是非常常見的,同樣可以成功調(diào)用執(zhí)行 run()
方法。
2.1.2 成員的訪問
成員內(nèi)部類可以直接訪問外部類的成員,例如,可以在內(nèi)部類的中訪問外部類的成員屬性:
// 外部類 Car
public class Car {
String name;
public Engine getEngine() {
return new Engine();
}
// 內(nèi)部類 Engine
private class Engine {
// 發(fā)動機的起動方法
private void run() {
System.out.println(name + "的發(fā)動機啟動了!");
}
}
public static void main(String[] args) {
// 實例化外部類
Car car = new Car();
// 為實例屬性賦值
car.name = "大奔奔";
// 獲取內(nèi)部類實例
Engine engine = car.getEngine();
// 調(diào)用內(nèi)部類的方法
engine.run();
}
}
觀察 Engine
的 run()
方法,調(diào)用了外部類的成員屬性 name
,我們在主方法實例化 Car
后,已經(jīng)為屬性 name
賦值。
運行結果:
大奔奔的發(fā)動機啟動了!
相同的,除了成員屬性,成員方法也可以自由訪問。這里不再贅述。
還存在一個同名成員的問題:如果內(nèi)部類中也存在一個同名成員,那么優(yōu)先訪問內(nèi)部類的成員。可理解為就近原則。
這種情況下如果依然希望訪問外部類的屬性,可以使用外部類名.this.成員
的方式,例如:
// 外部類 Car
public class Car {
String name;
public Engine getEngine() {
return new Engine();
}
// 汽車的跑動方法
public void run(String name) {
System.out.println(name + "跑起來了!");
}
// 內(nèi)部類 Engine
private class Engine {
private String name = "引擎";
// 發(fā)動機的起動方法
private void run() {
System.out.println("Engine中的成員屬性name=" + name);
System.out.println(Car.this.name + "的發(fā)動機啟動了!");
Car.this.run(Car.this.name);
}
}
public static void main(String[] args) {
// 實例化外部類
Car car = new Car();
// 為實例屬性賦值
car.name = "大奔奔";
// 獲取內(nèi)部類實例
Engine engine = car.getEngine();
// 調(diào)用內(nèi)部類的方法
engine.run();
}
}
運行結果:
Engine中的成員屬性name=引擎
大奔奔的發(fā)動機啟動了!
大奔奔跑起來了!
請觀察內(nèi)部類 run()
方法中的語句:第一行語句調(diào)用了內(nèi)部類自己的屬性 name
,而第二行調(diào)用了外部類 Car
的屬性 name
,第三行調(diào)用了外部類的方法 run()
,并將外部類的屬性 name
作為方法的參數(shù)。
2.2 靜態(tài)內(nèi)部類
2.2.1 定義
靜態(tài)內(nèi)部類也稱為嵌套類,是使用 static
關鍵字修飾的內(nèi)部類。如下代碼中定義了一個靜態(tài)內(nèi)部類:
public class Car1 {
// 靜態(tài)內(nèi)部類
static class Engine {
public void run() {
System.out.println("我是靜態(tài)內(nèi)部類的run()方法");
System.out.println("發(fā)動機啟動了");
}
}
}
2.2.2 實例化
靜態(tài)內(nèi)部類的實例化,可以不依賴外部類的對象直接創(chuàng)建。我們在主方法中可以這樣寫:
// 直接創(chuàng)建靜態(tài)內(nèi)部類對象
Engine engine = new Engine();
// 調(diào)用對象下run()方法
engine.run();
運行結果:
我是靜態(tài)內(nèi)部類的run()方法
發(fā)動機啟動
2.2.2 成員的訪問
在靜態(tài)內(nèi)部類中,只能直接訪問外部類的靜態(tài)成員。例如:
public class Car1 {
String brand = "寶馬";
static String name = "外部類的靜態(tài)屬性name";
// 靜態(tài)內(nèi)部類
static class Engine {
public void run() {
System.out.println(name);
}
}
public static void main(String[] args) {
Engine engine = new Engine();
engine.run();
}
}
在 run()
方法中,打印的 name
屬性就是外部類中所定義的靜態(tài)屬性 name
。編譯執(zhí)行,將會輸出:
外部類的靜態(tài)屬性name
對于內(nèi)外部類存在同名屬性的問題,同樣遵循就近原則。這種情況下依然希望調(diào)用外部類的靜態(tài)成員,可以使用外部類名.靜態(tài)成員
的方式來進行調(diào)用。這里不再一一舉例。
如果想要訪問外部類的非靜態(tài)屬性,可以通過對象的方式調(diào)用,例如在 run()
方法中調(diào)用 Car1
的實例屬性 brand
:
public void run() {
// 實例化對象
Car1 car1 = new Car1();
System.out.println(car1.brand);
}
2.3 方法內(nèi)部類
2.3.1 定義
方法內(nèi)部類,是定義在方法中的內(nèi)部類,也稱局部內(nèi)部類。
如下是方法內(nèi)部類的代碼:
public class Car2 {
// 外部類的run()方法
public void run() {
class Engine {
public void run() {
System.out.println("方法內(nèi)部類的run()方法");
System.out.println("發(fā)動機啟動了");
}
}
// 在Car2.run()方法的內(nèi)部實例化其方法內(nèi)部類Engine
Engine engine = new Engine();
// 調(diào)用Engine的run()方法
engine.run();
}
public static void main(String[] args) {
Car2 car2 = new Car2();
car2.run();
}
}
運行結果:
方法內(nèi)部類的run()方法
發(fā)動機啟動了
如果我們想調(diào)用方法內(nèi)部類的 run()
方法,必須在方法內(nèi)對 Engine
類進行實例化,再去調(diào)用其 run()
方法,然后通過外部類調(diào)用自身方法的方式讓內(nèi)部類方法執(zhí)行。
2.3.2 特點
與局部變量相同,局部內(nèi)部類也有以下特點:
- 方法內(nèi)定義的局部內(nèi)部類只能在方法內(nèi)部使用;
- 方法內(nèi)不能定義靜態(tài)成員;
- 不能使用訪問修飾符。
也就是說,Car2.getEngine()
方法中的 Engine
內(nèi)部類只能在其方法內(nèi)部使用;并且不能出現(xiàn) static
關鍵字;也不能出現(xiàn)任何的訪問修飾符,例如把方法內(nèi)部類 Engine
聲明為 public
是不合法的。
2.4 匿名內(nèi)部類
2.4.1 定義
匿名內(nèi)部類就是沒有名字的內(nèi)部類。使用匿名內(nèi)部類,通常令其實現(xiàn)一個抽象類或接口。請閱讀如下代碼:
// 定義一個交通工具抽象父類,里面只有一個run()方法
public abstract class Transport {
public void run() {
System.out.println("交通工具run()方法");
}
public static void main(String[] args) {
// 此處為匿名內(nèi)部類,將對象的定義和實例化放到了一起
Transport car = new Transport() {
// 實現(xiàn)抽象父類的run()方法
@Override
public void run() {
System.out.println("汽車跑");
}
};
// 調(diào)用其方法
car.run();
Transport airPlain = new Transport() {
// 實現(xiàn)抽象父類的run()方法
@Override
public void run() {
System.out.println("飛機飛");
}
};
airPlain.run();
}
}
運行結果:
汽車跑
飛機飛
上述代碼中的抽象父類中有一個方法 run()
,其子類必須實現(xiàn),我們使用匿名內(nèi)部類的方式將子類的定義和對象的實例化放到了一起,通過觀察我們可以看出,代碼中定義了兩個匿名內(nèi)部類,并且分別進行了對象的實例化,分別為 car
和 airPlain
,然后成功調(diào)用了其實現(xiàn)的成員方法 run()
。
2.4.2 特點
- 含有匿名內(nèi)部類的類被編譯之后,匿名內(nèi)部類會單獨生成一個字節(jié)碼文件,文件名的命名方式為:
外部類名稱$數(shù)字.class
。例如,我們將上面含有兩個匿名內(nèi)部類的Transport.java
編譯,目錄下將會生成三個字節(jié)碼文件:
Transport$1.class
Transport$2.class
Transport.class
- 匿名內(nèi)部類沒有類型名稱和實例對象名稱;
- 匿名內(nèi)部類可以繼承父類也可以實現(xiàn)接口,但二者不可兼得;
- 匿名內(nèi)部類無法使用訪問修飾符、
static
、abstract
關鍵字修飾; - 匿名內(nèi)部類無法編寫構造方法,因為它沒有類名;
- 匿名內(nèi)部類中不能出現(xiàn)靜態(tài)成員。
2.4.2 使用場景
由于匿名內(nèi)部類沒有名稱,類的定義可實例化都放到了一起,這樣可以簡化代碼的編寫,但同時也讓代碼變得不易閱讀。當我們在代碼中只用到類的一個實例、方法只調(diào)用一次,可以使用匿名內(nèi)部類。
3. 作用
3.1 封裝性
內(nèi)部類的成員通過外部類才能訪問,對成員信息有更好的隱藏,因此內(nèi)部類實現(xiàn)了更好的封裝。
3.2 實現(xiàn)多繼承
我們知道 Java 不支持多繼承,而接口可以實現(xiàn)多繼承的效果,但實現(xiàn)接口就必須實現(xiàn)里面所有的方法,有時候我們的需求只是實現(xiàn)其中某個方法,內(nèi)部類就可以解決這些問題。
下面示例中的 SubClass
,通過兩個成員內(nèi)部類分別繼承 SuperClass1
和 SuperClass2
,并重寫了方法,實現(xiàn)了多繼承:
// SuperClass1.java
public class SuperClass1 {
public void method1() {
System.out.println("The SuperClass1.method1");
}
}
// SuperClass2.java
public class SuperClass2 {
public void method2() {
System.out.println("The SuperClass2.method2");
}
}
// SubClass.java
public class SubClass {
// 定義內(nèi)部類1
class InnerClass1 extends SuperClass1 {
// 重寫父類1方法
@Override
public void method1() {
super.method1();
}
}
// 定義內(nèi)部類2
class InnerClass2 extends SuperClass2 {
// 重寫父類2方法
@Override
public void method2() {
super.method2();
}
}
public static void main(String[] args) {
// 實例化內(nèi)部類1
InnerClass1 innerClass1 = new SubClass().new InnerClass1();
// 實例化內(nèi)部類2
InnerClass2 innerClass2 = new SubClass().new InnerClass2();
// 分別調(diào)用內(nèi)部類1、內(nèi)部類2的方法
innerClass1.method1();
innerClass2.method2();
}
}
編譯執(zhí)行 SubClass.java
,屏幕將會打印:
$ javac SubClass.java
$ java SubClass
The SuperClass1.method1
The SuperClass1.method2
3.3 解決繼承或?qū)崿F(xiàn)接口時的方法同名問題
請閱讀如下代碼:
// One.java
public class One {
public void test() {
}
}
// Two.java
public interface Two {
void test();
}
// Demo.java
public class Demo1 extends One implements Two {
public void test() {
}
}
此時,我們無法確定 Demo1
類中的 test()
方法是父類 One
中的 test
還是接口 Two
中的 test
。這時我們可以使用內(nèi)部類解決這個問題:
public class Demo2 extends One {
// 重寫父類方法
@Override
public void test() {
System.out.println("在外部類實現(xiàn)了父類的test()方法");
}
// 定義內(nèi)部類
class InnerClass implements Two {
// 重寫接口方法
@Override
public void test() {
System.out.println("在內(nèi)部類實現(xiàn)了接口的test()方法");
}
}
public static void main(String[] args) {
// 實例化子類Demo2
Demo2 demo2 = new Demo2();
// 調(diào)用子類方法
demo2.test();
// 實例化子類Demo2的內(nèi)部類
InnerClass innerClass = demo2.new InnerClass();
// 調(diào)用內(nèi)部類方法
innerClass.test();
}
}
運行結果:
在外部類實現(xiàn)了父類的test()方法
在內(nèi)部類實現(xiàn)了接口的test()方法
4. 小結
本小節(jié),我們知道了什么是內(nèi)部類,也知道了在 Java 中有四種內(nèi)部類:成員內(nèi)部類、靜態(tài)內(nèi)部類、方法內(nèi)部類和匿名內(nèi)部類。對于它們的定義和調(diào)用也做了詳細講解,理解內(nèi)部類的作用是使用好內(nèi)部類的關鍵。