Java——內部類詳解
- 2019 年 12 月 9 日
- 筆記
說起內部類,大家肯定感覺熟悉又陌生,因為一定在很多框架源碼中有看到別人使用過,但又感覺自己使用的比較少,今天我就帶你具體來看看內部類。
內部類基礎
所謂內部類就是在類的內部繼續定義其他內部結構類。
在 Java 中,廣泛意義上的內部類一般來說包括這四種:成員內部類、局部內部類、匿名內部類和靜態內部類。下面就先來了解一下這四種內部類的用法。
成員內部類
成員內部類是最普通的內部類,它的定義為位於另一個類的內部,具體使用如下:
class Circle { double radius = 0; public Circle(double radius) { this.radius = radius; } /** * 內部類 */ class Draw { public void drawSahpe() { System.out.println("drawshape"); } } }
這樣看起來,類 Draw 像是類 Circle 的一個成員, Circle 稱為外部類。成員內部類可以無條件訪問外部類的所有成員屬性和成員方法(包括 private 成員和靜態成員),例如:
class Circle { private double radius = 0; public static int count =1; public Circle(double radius) { this.radius = radius; } /** * 內部類 */ class Draw { public void drawSahpe() { // 外部類的private成員 System.out.println(radius); // 外部類的靜態成員 System.out.println(count); } } }
不過要注意的是,當成員內部類擁有和外部類同名的成員變量或者方法時,會發生隱藏現象,即默認情況下訪問的是成員內部類的成員。如果要訪問外部類的同名成員,需要採取以下形式進行訪問:
外部類.this.成員變量 外部類.this.成員方法
雖然成員內部類可以無條件地訪問外部類的成員,而外部類想訪問成員內部類的成員卻不是這麼隨心所欲了。在外部類中如果要訪問成員內部類的成員,必須先創建一個成員內部類的對象,再通過指向這個對象的引用來訪問,其具體形式為:
class Circle { private double radius = 0; public Circle(double radius) { this.radius = radius; // 必須先創建成員內部類的對象,再進行訪問 getDrawInstance().drawSahpe(); } private Draw getDrawInstance() { return new Draw(); } /** * 內部類 */ class Draw { public void drawSahpe() { // 外部類的private成員 System.out.println(radius); } } }
成員內部類是依附外部類而存在的,也就是說,如果要創建成員內部類的對象,前提是必須存在一個外部類的對象。創建成員內部類對象的一般方式如下:
public class Test { public static void main(String[] args) { // 第一種方式 Outter outter = new Outter(); // 必須通過Outter對象來創建 Outter.Inner inner = outter.new Inner(); // 第二種方式 Outter.Inner inner1 = outter.getInnerInstance(); } } class Outter { private Inner inner = null; public Outter() { } public Inner getInnerInstance() { if(inner == null) inner = new Inner(); return inner; } class Inner { public Inner() { } } }
內部類可以擁有 private 訪問權限、 protected 訪問權限、 public 訪問權限及包訪問權限。
比如上面的例子,如果成員內部類 Inner 用 private 修飾,則只能在外部類的內部訪問;如果用 public 修飾,則任何地方都能訪問;如果用 protected 修飾,則只能在同一個包下或者繼承外部類的情況下訪問;如果是默認訪問權限,則只能在同一個包下訪問。
這一點和外部類有一點不一樣,外部類只能被 public 和包訪問兩種權限修飾。
我個人是這麼理解的,由於成員內部類看起來像是外部類的一個成員,所以可以像類的成員一樣擁有多種權限修飾。
局部內部類
局部內部類是定義在一個方法或者一個作用域裏面的類,它和成員內部類的區別在於局部內部類的訪問僅限於方法內或者該作用域內。
class People{ public People() { } } class Man{ public Man(){ } public People getWoman(){ /** * 局部內部類 */ class Woman extends People{ int age =0; } return new Woman(); } }
注意,局部內部類就像是方法裏面的一個局部變量一樣,是不能用 public 、 protected 、 private 以及 static 修飾的。
匿名內部類
匿名內部類應該是平時我們編寫代碼時用得最多的,比如創建一個線程的時候:
class Test { public static void main(String[] args) { Thread thread = new Thread( // 匿名內部類 new Runnable() { @Override public void run() { System.out.println("Thread run"); } } ); } }
同樣的,匿名內部類也是不能有訪問修飾符和 static 修飾符的。
匿名內部類是唯一一種沒有構造器的類。正因為其沒有構造器,所以匿名內部類的使用範圍非常有限,大部分匿名內部類用於接口回調。
匿名內部類在編譯的時候由系統自動起名為Outter$1.class
。一般來說,匿名內部類用於繼承其他類或是實現接口,並不需要增加額外的方法,只是對繼承方法的實現或是重寫。
靜態內部類
靜態內部類也是定義在另一個類裏面的類,只不過在類的前面多了一個關鍵字 static 。
靜態內部類是不需要依賴於外部類的,這點和類的靜態成員屬性有點類似,並且它不能使用外部類的非 static 成員變量或者方法,這點很好理解,因為在沒有外部類的對象的情況下,可以創建靜態內部類的對象,如果允許訪問外部類的非 static 成員就會產生矛盾,因為外部類的非 static 成員必須依附於具體的對象。
例如:
public class Test { public static void main(String[] args) { Outter.Inner inner = new Outter.Inner(); } } class Outter { public Outter() { } /** * 靜態 */ static class Inner { public Inner() { } } }
深入理解內部類
通過上面的介紹,相比你已經大致了解的內部類的使用,那麼你的心裏想必會有一個疑惑:
為什麼成員內部類可以無條件訪問外部類的成員?
首先我們先定義一個內部類:
public class Outter { private Inner inner = null; public Outter() { } public Inner getInnerInstance() { if (inner == null) inner = new Inner(); return inner; } protected class Inner { public Inner() { } } }
先用 javac 進行編譯,你可以發現會生成兩個文件:Outter$Inner.class 和 Outter.class 。接下來利用javap -p
反編譯 Outter$Inner.class ,其結果如下:
Classfile /D:/project/Test/src/test/java/test/Outter$Inner.class Last modified 2019-11-25; size 408 bytes MD5 checksum b936e37bc77059b83951429e28f3f225 Compiled from "Outter.java" public class Outter$Inner minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPERConstant pool: #1 = Fieldref #3.#13 // test/Outter$Inner.this$0:Ltest/Outter; #2 = Methodref #4.#14 // java/lang/Object."<init>":()V #3 = Class #16 // test/Outter$Inner #4 = Class #19 // java/lang/Object #5 = Utf8 this$0 #6 = Utf8 Ltest/Outter; #7 = Utf8 <init> #8 = Utf8 (Ltest/Outter;)V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 SourceFile #12 = Utf8 Outter.java #13 = NameAndType #5:#6 // this$0:Ltest/Outter; #14 = NameAndType #7:#20 // "<init>":()V #15 = Class #21 // test/Outter #16 = Utf8 test/Outter$Inner #17 = Utf8 Inner #18 = Utf8 InnerClasses #19 = Utf8 java/lang/Object #20 = Utf8 ()V #21 = Utf8 test/Outter { final Outter this$0; descriptor: Ltest/Outter; flags: ACC_FINAL, ACC_SYNTHETIC public Outter$Inner(Outter); descriptor: (Ltest/Outter;)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:Ltest/Outter; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."<init>":()V 9: return LineNumberTable: line 16: 0 line 17: 9 } SourceFile: "Outter.java" InnerClasses: protected #17= #3 of #15; //Inner=class test/Outter$Inner of class test/Outter
32行的內容為:final Outter this$0;
學過 C 的朋友應該能知道,這是一個指向外部類 Outter 對象的指針,也就是說編譯器會默認為成員內部類添加一個指向外部類對象的引用,這樣也就解釋了為什麼成員內部類能夠無條件訪問外部類了。
那麼這個引用是如何賦初值的呢?下面接着看內部類的構造器:public Outter$Inner(Outter);
從這裡可以看出,雖然我們在定義的內部類的構造器是無參構造器,但編譯器還是會默認添加一個參數,該參數的類型為指向外部類對象的一個引用,所以成員內部類中的 Outter this&0 指針便指向了外部類對象,因此可以在成員內部類中隨意訪問外部類的成員。
從這裡也間接說明了成員內部類是依賴於外部類的,如果沒有創建外部類的對象,則無法對 Outter this&0 引用進行初始化賦值,也就無法創建成員內部類的對象了。
為什麼局部內部類和匿名內部類只能訪問局部final變量?
我們還是採用和之前一樣的解答方式,先定義一個類:
public class Outter { public static void main(String[] args) { Outter outter = new Outter(); int b = 10; outter.test(b); } public void test(final int b) { final int a = 10; new Thread(){ public void run() { System.out.println(a); System.out.println(b); }; }.start(); } }
通過 javac 編譯 Outter,也會生成兩個文件:Outter.class 和 Outter1.class。默認情況下,編譯器會為匿名內部類和局部內部類起名為 Outter$x.class( x 為正整數)。
根據我提供的類,可以思考一個問題:
當 test 方法執行完畢之後,變量 a 的生命周期就結束了,而此時 Thread 對象的生命周期很可能還沒有結束,那麼在 Thread 的 run 方法中繼續訪問變量 a 就變成不可能了,但是又要實現這樣的效果,怎麼辦呢?
Java 採用了複製
的手段來解決這個問題。將 Outter$1.class 反編譯可以得到下面的內容:
Classfile /D:/project/Test/src/test/java/test/Outter$1.class Last modified 2019-11-25; size 653 bytes MD5 checksum 2e238dafbd73356eba22d473c6469082 Compiled from "Outter.java" class test.Outter$1 extends java.lang.Thread minor version: 0 major version: 52 flags: ACC_SUPERConstant pool: #1 = Fieldref #6.#23 // test/Outter$1.this$0:Ltest/Outter; #2 = Fieldref #6.#24 // test/Outter$1.val$b:I #3 = Methodref #7.#25 // java/lang/Thread."<init>":()V #4 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream; #5 = Methodref #28.#29 // java/io/PrintStream.println:(I)V #6 = Class #30 // test/Outter$1 #7 = Class #32 // java/lang/Thread #8 = Utf8 val$b #9 = Utf8 I #10 = Utf8 this$0 #11 = Utf8 Ltest/Outter; #12 = Utf8 <init> #13 = Utf8 (Ltest/Outter;I)V #14 = Utf8 Code #15 = Utf8 LineNumberTable #16 = Utf8 run #17 = Utf8 ()V #18 = Utf8 SourceFile #19 = Utf8 Outter.java #20 = Utf8 EnclosingMethod #21 = Class #33 // test/Outter #22 = NameAndType #34:#35 // test:(I)V #23 = NameAndType #10:#11 // this$0:Ltest/Outter; #24 = NameAndType #8:#9 // val$b:I #25 = NameAndType #12:#17 // "<init>":()V #26 = Class #36 // java/lang/System #27 = NameAndType #37:#38 // out:Ljava/io/PrintStream; #28 = Class #39 // java/io/PrintStream #29 = NameAndType #40:#35 // println:(I)V #30 = Utf8 test/Outter$1 #31 = Utf8 InnerClasses #32 = Utf8 java/lang/Thread #33 = Utf8 test/Outter #34 = Utf8 test #35 = Utf8 (I)V #36 = Utf8 java/lang/System #37 = Utf8 out #38 = Utf8 Ljava/io/PrintStream; #39 = Utf8 java/io/PrintStream #40 = Utf8 println { final int val$b; descriptor: I flags: ACC_FINAL, ACC_SYNTHETIC final test.Outter this$0; descriptor: Ltest/Outter; flags: ACC_FINAL, ACC_SYNTHETIC test.Outter$1(test.Outter, int); descriptor: (Ltest/Outter;I)V flags: Code: stack=2, locals=3, args_size=3 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:Ltest/Outter; 5: aload_0 6: iload_2 7: putfield #2 // Field val$b:I 10: aload_0 11: invokespecial #3 // Method java/lang/Thread."<init>":()V 14: return LineNumberTable: line 10: 0 public void run(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 3: bipush 10 5: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 11: aload_0 12: getfield #2 // Field val$b:I 15: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 18: return LineNumberTable: line 12: 0 line 13: 8 line 14: 18 } SourceFile: "Outter.java" EnclosingMethod: #21.#22 // test.Outter.test InnerClasses: #6; //class test/Outter$1
我們看到在 run 方法中有一條指令:bipush 10
這條指令表示將操作數10壓棧,表示使用的是一個本地局部變量。
這個過程是在編譯期間由編譯器默認進行,如果這個變量的值在編譯期間可以確定,則編譯器默認會在匿名內部類(局部內部類)的常量池中添加一個內容相等的字面量或直接將相應的位元組碼嵌入到執行位元組碼中。
這樣一來,匿名內部類使用的變量是另一個局部變量,只不過值和方法中局部變量的值相等,因此和方法中的局部變量完全獨立開。
接下來也來看一下 test.Outter$1 的構造方法:test.Outter$1(test.Outter, int);
我們看到匿名內部類 Outter$1 的構造器含有兩個參數,一個是指向外部類對象的引用,一個是 int 型變量,很顯然,這裡是將變量 test 方法中的形參 b 以參數的形式傳進來對匿名內部類中的拷貝(變量 b 的拷貝)進行賦值初始化。
也就說如果局部變量的值在編譯期間就可以確定,則直接在匿名內部裏面創建一個拷貝。如果局部變量的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。
從上面可以看出,在 run 方法中訪問的變量 b 根本就不是test方法中的局部變量 b 。這樣一來就解決了前面所說的 生命周期不一致的問題。但是新的問題又來了,既然在 run 方法中訪問的變量 b 和test方法中的變量 b 不是同一個變量,那麼當在 run 方法中改變變量 b 的值的話,會出現什麼情況?
會造成數據不一致性
,這樣就達不到原本的意圖和要求。為了解決這個問題, Java 編譯器就限定必須將變量 b 限制為 final ,不允許對變量 b 進行更改(對於引用類型的變量,是不允許指向新的對象),這樣數據不一致性的問題就得以解決了。
到這裡,想必大家應該清楚為何 方法中的局部變量和形參都必須用 final 進行限定了。
靜態內部類有特殊的地方嗎?
從前面可以知道,靜態內部類是不依賴於外部類的,也就說可以在不創建外部類對象的情況下創建內部類的對象。
另外,靜態內部類是不持有指向外部類對象的引用的,這個讀者可以自己嘗試反編譯 class 文件看一下就知道了,是沒有 Outter this&0 引用的。
總結
今天介紹了內部類相關的知識,包括其一般的用法以及內部類和外部類的依賴關係,通過對位元組碼進行反編譯詳細了解了其實現模式,最後留給大家一個任務自己去實際探索一下靜態內部類的實現。希望通過這篇介紹可以幫大家更加深刻了解內部類。
有興趣的話可以訪問我的博客或者關注我的公眾號、頭條號,說不定會有意外的驚喜。
https://death00.github.io/