目錄
一. 前言
二.?Java 類的創建要求
三. Java 內部類分類
3.1. 成員內部類
3.1.1. 特點
3.1.2. 語法
3.1.3. 代碼示例
3.1.4. 代碼測試
3.1.5.?關于 this 的注意事項
3.1.6. 小結
3.2. 局部內部類
3.2.1. 特點
3.2.2. 語法
3.2.3. 代碼示例
3.2.4. 代碼測試
3.2.5.?Effectively final 特性
3.3. 匿名內部類
3.3.1. 特點
3.3.2. 語法
3.3.3. 代碼示例
3.3.4. 代碼測試
3.4. 靜態內部類
3.4.1. 特點
3.4.2.?語法
3.4.3. 代碼示例
3.4.4. 代碼測試
3.4.5. 小結
四. 總結
一. 前言
? ? 在 Java 中,我們通常是把不同的類創建在不同的包里面,對于同一個包里的類來說,它們都是同一層次的。但其實還有另一種情況,有些類可以被定義在另一個類的內部,我們把在一個類里面定義的類稱為內部類(InnerClass)或嵌套類,把外面定義的類稱為外部類(OutClass)或宿主類。
? ? 也就是說,在類的內部既可以定義成員變量和方法,也可以定義其他的類。定義內部類的常見格式如下:
public class Outer { // 外部類class Inner { // 內部類// 方法和屬性}
}
上面的代碼中,Outer 是普通的外部類,Inner就是內部類。它與普通外部類最大的不同,在于其 實例對象不能單獨存在,必須依附于一個外部類的實例對象。
? ? 內部類可以很好地實現隱藏,一般的非內部類是不允許有 private 與 protected 權限的,但內部類卻可以,而且內部類還擁有外部類中所有元素的訪問權限。總之,對內部類的很多訪問規則都可以參考變量和方法。
? ? 但是要注意,雖然我們使用內部類可以使程序結構變得更加緊湊,但卻在一定程度上破壞了面向對象的思想。
二.?Java 類的創建要求
我們在創建定義Java類時,應該遵循如下要求:
- 一個 Java 文件中可以編寫多個類,但只能有一個類使用 public 關鍵詞進行修飾,這稱之為主類;
- 主類名必須與文件名一致,在開發中,應盡量只在一個 Java 文件中編寫一個類;
- 外部類只有兩種訪問級別:public 和 default(默認);內部類則有 4 種訪問級別:public、protected、 private 和 default(默認);
- 在外部類中,可以直接通過內部類的類名來訪問內部類;
- 在外部類以外的其他類中,需要通過內部類的完整類名來訪問內部類;
- 內部類與外部類不能重名。
三. Java 內部類分類
? ? Java 中的內部類可以分為如下幾種類型:成員內部類、靜態內部類、局部內部類、匿名內部類。雖然大多數時候,內部類用得并不多,但我們也有必要了解它們是如何具體使用的。
3.1. 成員內部類
成員內部類就是指沒有被 static 修飾的內部類,也可以稱為非靜態內部類。
3.1.1. 特點
成員內部類具有如下特點:
- 在 JDK16?版本以前,成員內部類中只能定義非靜態的屬性和方法,除非同時使用 final 和static 進行修飾;
- 在 JDK16 及之后的版本中,成員內部類中也可以定義靜態的屬性和方法;
- 成員內部類可以訪問外部類的所有成員,包括私有和靜態的成員,即使是多層嵌套時也如此;
- 外部類不能直接訪問內部類的成員,必須通過內部類的實例對象訪問;
- 在外部類的靜態方法和外部類以外的其他類中,必須通過外部類的實例創建內部類的實例對象;
- 外部類的實例與內部類實例是一對多的關系,即一個內部類實例只對應一個外部類實例,但一個外部類實例則可以對應多個內部類實例。
3.1.2. 語法
如果是在外部類中,創建成員內部類對象的基本語法格式如下:
// 內部類 對象名 = new 內部類();
Inner inner = new Inner();
如果是在外部的其他類中,或者是在外部類的靜態方法中,創建成員內部類對象的基本語法格式如下:
// 內部類 對象名 = new 外部類().new 內部類();
Inner inner = new Outer().new Inner();
3.1.3. 代碼示例
/*** 成員內部類*/
public class OuterClass {// 外部類的非靜態成員String name = "流華追夢";private String hobby = "寫代碼";static int age = 28;// 非靜態方法public void show() {// 這里的 this 是指 OuterClass 對象System.out.println("show 方法,name = " + this.name);// 如果是在外部類里面創建內部類的對象,就不需要創建外部類實例,可以直接 new 內部類();// InnerClass inner = new InnerClass();}// 定義一個成員內部類public class InnerClass {// 也可以定義私有屬性private int a = 10;// 在 JDK16 版本以前,成員內部類中不能定義靜態變量;在 JDK16 及之后的版本中,成員內部類中可以定義靜態變量。static int b = 20;// 非靜態方法public void testInner1() {// 這里的 this 是指 InnerClass 內部類對象System.out.println("成員內部類的成員變量:" + this.a);// 外部類.this.屬性或方法,這個 this 是外部類對象System.out.println("外部類的成員變量:" + OuterClass.this.name);// 內部類中可以訪問外部類的私有成員和靜態成員System.out.println("外部類的私有成員變量:" + hobby);System.out.println("外部類的靜態變量:" + age);}// 在 JDK16 版本以前,成員內部類中不能定義靜態方法;在 JDK16 及之后的版本中,成員內部類中可以定義靜態方法。public static void testInner2() {System.out.println("調用成員內部類的靜態變量:" + b);System.out.println("調用外部類的靜態變量:" + age);// 在靜態方法中創建內部類對象,也要通過 內部類 對象名 = new 外部類().new 內部類(); 的格式InnerClass innerClass = new OuterClass().new InnerClass();}}
}
我們要注意,在 JDK16 版本以前,成員內部類中不能定義靜態屬性和方法;但在 JDK16 及之后的版本中,成員內部類中可以定義靜態的屬性和方法。并且我們要搞清楚在不同的位置上,創建內部類對象的方式,以及 this 的具體含義。
3.1.4. 代碼測試
我們在外部的其他類中,要想創建出一個成員內部類的對象,需要通過如下形式:
// 內部類 對象名 = new 外部類().new 內部類();
Inner inner = new Outer().new Inner();
public class InnerClassTest {public static void main(String[] args) {// 在外部的其他類中,不能直接創建內部類對象,否則:// No enclosing instance of type OuterClass is accessible.// Must qualify the allocation with an enclosing instance of type OuterClass// (e.g. x.new A() where x is an instance of OuterClass).// InnerClass inner = new InnerClass();// 在外部的其他類中創建內部類對象,需要通過如下格式:// 內部類 對象名 = new 外部類().new 內部類();// InnerClass inner = new OuterClass().new InnerClass();// 也可以拆分成如下格式:OuterClass outer = new OuterClass();InnerClass inner = outer.new InnerClass();inner.testInner1();InnerClass.testInner2();}
}
3.1.5.?關于 this 的注意事項
在內部類中,關于 this,我們需要注意以下兩點:
- 如果同時存在外部類和內部類,那么 this 在哪個類中使用,this 就代表哪個類的對象;
- 如果內部類想要通過 this 來調用外部類的屬性和方法,需要使用 外部類名.this.屬性或者方法名。
3.1.6. 小結
? ? 學習到這里,你可能會被內部類與外部類之間的調用訪問關系整蒙圈,所以給大家梳理了一下訪問方式:
1. 成員內部類 訪問 外部類的成員(屬性、方法),可以【直接訪問使用】;
2. 外部類 訪問 成員內部類,需要【直接創建內部類對象后再訪問】,即 new InnerClass();
3. 外部的其他類 訪問 成員內部類,需要【創建外部類對象,再創建內部類對象后訪問】,即 InnerClass inner = new OuterClass().new InnerClass();
3.2. 局部內部類
局部內部類是指在方法中定義的內部類。
3.2.1. 特點
局部內部類具有如下特點:
- 局部內部類只能在方法中定義和創建對象,也只在當前方法中有效;
- 局部內部類中可以訪問外部類的所有成員;
- 局部內部類與局部變量一樣,不能使用訪問控制修飾符(public、private、protected)和 static 修飾符;
- 在 JDK7 版本中,如果局部變量是在局部內部類中使用,必須顯式地加上 final 關鍵字;在 JDK8 及之后版本中,會默認添加 final 關鍵字;
- 局部內部類只能訪問當前方法中 final 類型的參數與變量。如果方法中的成員與外部類的成員同名,可以使用 <OuterClassName>.this.<MemberName> 的形式訪問外部類成員;
- 局部內部類中還可以包含內部類,但這些內部類也不能使用訪問控制修飾符(public、private 、protected)和 static 修飾符;
- 局部變量在方法執行結束后會被銷毀,而局部內部類的對象會等到內存回收機制進行銷毀。如果是局部內部類里的常量,該常量會被存放在常量池中。
3.2.2. 語法
創建局部內部類對象的基本語法格式如下:
public class PartClass {public void methodA() {// 在方法中定義的內部類,就是局部內部類class Inner {// 屬性// 方法}}
}
3.2.3. 代碼示例
public class PartOuterClass {// 類的成員變量String name = "流華追夢";private int age = 28;static String hobby = "Java";public void show() {// 局部變量// JDK7 之前,匿名內部類和局部內部類中訪問外部的局部變量時,該變量需要明確地帶有 final 修飾符// final int num = 10;// Effectively final 特性int num = 10;// 局部內部類,類似于是方法中的局部對象class PartInnerClass {// 內部可以正常定義方法public void testInner1() {// 訪問外部類的非靜態成員,可以使用 OuterClass.this.成員 的格式,也可以直接訪問// System.out.println("外部類的成員變量:" + name);System.out.println("外部類的成員變量:" + PartOuterClass.this.name);System.out.println("外部類私有的成員變量:" + age);System.out.println("外部類的靜態變量:" + hobby);// 局部內部類,可以直接訪問方法中的局部變量System.out.println("訪問局部變量:" + num);}// 在 JDK16 及之后的版本中,也可以定義靜態屬性和靜態方法,但之前的 JDK 版本則不行static int b = 10;public static void testInner2() {System.out.println("外部類的靜態變量,hobby = " + hobby + ",b = " + b);}}// 創建局部內部類對象PartInnerClass inner = new PartInnerClass();inner.testInner1();// 在當前類中,局部內部類可以直接訪問靜態成員PartInnerClass.testInner2();}
}
在 JDK7 之前,匿名內部類和局部內部類中訪問外部的局部變量時,該變量需要明確地帶有 final 修飾符。但從 JDK8 之后,我們可以不帶 final 修飾符,而是由系統默認添加了。
3.2.4. 代碼測試
接下來我們對上面的示例進行測試:
public class PartInnerClassTest {public static void main(String[] args) {// 創建外部類對象,調用方法,執行局部內部類PartOuterClass outer = new PartOuterClass();outer.show();}
}
3.2.5.?Effectively final 特性
? ? 一般情況下,Java 中的局部內部類和匿名內部類訪問局部變量時,該變量必須由 final 修飾,以保證內部類和外部類的數據一致性。但從 Java8 開始,我們可以不加 final 修飾符,而是由系統默認添加,當然這在 Java8 以前是不允許的。Java 將這個新的特性稱為 Effectively(有效的、實際的)final 功能。
另外在 lambda 表達式中,使用局部變量時也要求該變量必須是 final 修飾的,所以 effectively final特性在 lambda 表達式的上下文中非常有用。
其實 effectively final 特性,只是讓我們不用顯式地把變量聲明為 final 修飾的,它給我們自動添加了 final 修飾詞,但并沒有取消 final,主要是減少了一點不必要的操作,給開發節省了點時間。
3.3. 匿名內部類
? ? 匿名內部類就是指沒有類名的內部類,必須在創建時使用 new 語句來聲明。匿名內部類不能在Outer Class 外部類中定義,而是要在某個方法的內部,通過匿名類(Anonymous Class)的形式來定義。匿名內部類本身就是一個對象。
? ? 通常情況下,如果一個方法的參數是接口類型,且該接口只需要實現一次,那么我們就可以通過匿名內部類的形式來進行定義。另外如果該接口的實現每次都不同,也可以使用匿名內部類的形式進行定義。我們也可以把這種定義形式叫做“接口回調”。匿名內部類的代碼格式使得代碼更加簡潔、緊湊,模塊化程度也更高。
3.3.1. 特點
匿名內部類具有如下特點:
- 匿名內部類本身就是一個對象;
- 一般在匿名內部類中不會定義屬性和方法,因為沒有意義;
- 匿名內部類的父類一般都是抽象類或者是接口;
- 匿名內部類和局部內部類一樣,可以訪問外部類的所有成員;
- 如果匿名內部類位于方法中,則該類只能訪問方法中 final 類型的局部變量和參數;
- 匿名內部類中允許使用非靜態代碼塊對成員進行初始化操作;
- 匿名內部類的非靜態代碼塊會在父類的構造方法之后被執行。
3.3.2. 語法
通常匿名內部類有兩種實現方式:
- 繼承一個類,重寫其方法;
- 實現一個或多個接口,并實現其方法。
創建匿名內部類對象的基本語法格式如下:
new <類或接口>() {// 重寫類或接口的方法
}// 典型示例:
new Runnable() {@Overridepublic void run() {}
}
3.3.3. 代碼示例
? ? 為了給大家演示匿名內部類的用法,接下來設計一個模擬按鈕點擊事件的案例。當我們進行安卓等設備開發時,面板上有個按鈕,點擊該按鈕,如何監聽點擊事件?在 Android 系統中提供了各種對應的按鈕點擊監聽事件。這里就通過實現接口的形式來定義匿名內部類,模擬一個單擊事件。
?首先我們需要定義一個接口,表示單擊監聽,內部有個點擊事件:
/*** 點擊監聽事件*/
public interface OnClickListener {// 點擊事件void onClick();
}
然后定義一個 Button 按鈕類,給 Button 按鈕安排一個點擊監聽方法:
/*** 局部內部類 ---- 定義在方法中的內部類*/
public class Button {// 處理案例點擊的監聽事件public void setOnClickListener(OnClickListener listener) {listener.onClick();}
}
3.3.4. 代碼測試
/*** 匿名內部類測試*/
public class AnonymousInnerClassTest {public static void main(String[] args) {// 外部變量int num = 20;// 測試匿名內部類Button btn = Button();// 模擬處理按鈕的點擊事件btn.setOnClickListener(new OnClickListener() { // 這里就是一個匿名內部類// 在匿名內部類中,可以允許使用非靜態代碼塊進行成員初始化操作。int i;{ // 非靜態代碼塊,在構造方法之后執行i = 100; // 成員初始化}@Overridepublic void onClick() {System.out.println("按鈕被點擊啦!i = " + i + ",num = " + num);}});}
}
根據上面的案例可知:
- 在匿名內部類中,可以允許使用非靜態代碼塊進行成員初始化操作;
- 匿名內部類的非靜態代碼塊,會在構造方法之后執行;
- 匿名內部類也可以直接使用外部類的成員。
3.4. 靜態內部類
? ? 靜態內部類和成員內部類的定義類似,但要使用 static 修飾,所以稱為靜態內部類(Static Nested Class)。
? ? 靜態內部類和成員內部類有很大的不同,它不再依附于 Outer 的實例,而是一個完全獨立的類,因此無法引用 Outer.this 的方式調用。但它可以訪問 Outer 類的 private 靜態字段和靜態方法,如果我們把靜態內部類移到 Outer 類之外,就失去了訪問 private 的權限。
3.4.1. 特點
- 靜態內部類中可以定義非靜態的屬性和方法,也可以定義靜態的屬性和方法;
- 靜態內部類中只能訪問靜態外部類的靜態屬性和方法。
3.4.2.?語法
創建靜態內部類對象的基本語法格式如下:
// 內部類 對象名 = new 外部類.內部類();
Inner inner = new Outer.Inner();
3.4.3. 代碼示例
? ? 這里先簡單定義一個靜態內部類,在這個靜態內部類中,定義了一個方法,來訪問外部類中的普通屬性和靜態屬性。我們要記住以下幾點:
- 靜態內部類訪問外部類的成員變量時,需要先創建外部類對象;
- 非靜態內部類可以直接訪問使用外部類的成員變量,如同使用本類中的變量;
- 所有的內部類訪問外部類的靜態變量時,可以直接通過 外部類.靜態變量 的形式訪問。
/*** 靜態內部類*/
public class OuterClass {// 普通屬性,屬于外部類int outerAge = 28;static int outerNum = 10;// 定義一個靜態的內部類,如果不帶 static,就是一個普通的內部類。// 內部類的使用,和普通類一樣,里面可以正常定義屬性、方法、構造方法等。// static 前面可以帶 public 等任意訪問修飾符,也可以不帶。static class InnerClass {// 私有屬性無法在類的外部直接訪問// private int innerNum = 20;int innerNum = 20;public void printNum() {// 定義外部類對象OuterClass outer = new OuterClass();// 這里的 this 是指 InnerClass 內部類對象!System.out.println("innerNum = " + this.innerNum + ",outerAge = " + outer.outerAge + ",outerNum = " + OuterClass.outerNum);}}
}
對于靜態內部類而言,static 前面可以帶 public 等任意訪問修飾符,也可以不帶。
3.4.4. 代碼測試
我們再定義一個測試類,看看內部類對象是怎么調用的:
/*** 測試訪問靜態內部類*/
public class InnerClassTest {public static void main(String[] args) {// 創建內部類對象,格式為:外部類.內部類 對象名 = new 外部類.內部類的構造方法OuterClass.InnerClass inner = new OuterClass.InnerClass();// 調用內部類的方法inner.printNum();// 訪問外部類屬性System.out.println("outerNum = " + OuterClass.outerNum);// 訪問內部類屬性System.out.println("innerNum = " + inner.innerNum);}
}
3.4.5. 小結
對于靜態內部類的訪問要求,給大家總結如下:
1. 靜態內部類中可以直接訪問外部類的所有靜態方法,包含私有的,但不能直接訪問非靜態成員;
2. 靜態內部類可以添加任意訪問修飾符(public、protected、default(默認)、private),因為它的地位就是一個成員;
3. 如果靜態內部類 訪問 外部類 的靜態屬性、靜態方法等,訪問方式是【直接訪問】;
4. 如果外部類或外部的其他類來 訪問 靜態內部類,訪問方式是【外部類.內部類 對象名 = new 外部類.內部類的構造方法】,創建出內部類對象后再訪問。
四. 總結
我們來總結一下內部類的重點內容:
- 內部類分為成員內部類、局部內部類、匿名內部類和靜態內部類;
- 成員內部類和匿名內部類在本質上是相同的,都必須依附于外部類的實例,會隱含地持有Outer.this 實例,并擁有外部類的 private 訪問權限;
- 靜態內部類是獨立類,但擁有外部類的 private 訪問權限;
- 如果外部類和內部類中的成員重名時,內部類訪問時默認會遵循就近原則;如果想訪問外部類的成員,則可以用【外部類名.成員】的形式來訪問。
內部類的存在,具有如下優點:
- 內部類使得多繼承的解決方案變得更完整:每個內部類都能獨立的實現接口,無論外部類是否已經實現了接口或繼承了父類,對于內部類都沒有影響;
- 既可以方便地將存在一定邏輯關系的類組織在一起,又可以對外界隱藏;
- 方便各類編寫事件驅動程序;
- 方便編寫線程代碼。?