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