Java語法糖(二)
語法糖之四:內部類
內部類:顧名思義,在類的內部在定義一個類。內部類僅僅是編譯時的概念,編譯成字節碼後,內部類會生成單獨的Class文件。
四種:成員內部類、局部內部類、匿名內部類、靜態內部類。
1、成員內部類(member inner class)
常見用法:1、List、Set集合中的叠代器類;
栗子:
public class OuterClass { private String str; public OuterClass(String str) { this.str = str; } privatevoid print() { System.out.println("OuterClass print : " + str); } class InnerClass { //成員內部類不能含有static變量和方法 //成員內部類依附於外部類,只有先創建外部類才能創建內部類 private String strInner; public void printInner() { //內部類可以訪問外部類的屬性,即使是私有變量, //如果內部類的成員變量與外部類的成員變量重名,內部類的str會覆蓋外部類的strSystem.out.println("InnerClass print : " + str); //內部類可以訪問外部類的方法,即使是私有方法 //如果內部類的方法與外部類的方法重名,內部類的print會覆蓋外部類的print,會出現無限遞歸的情況. print(); } } //獲取內部類 public InnerClass getInnerClass() { return new InnerClass(); } publicstatic void main(String[] args) { OuterClass outerClass = new OuterClass("hello world"); OuterClass.InnerClass innerClass = outerClass.getInnerClass(); //外部類訪問內部類的成員變量和方法,需要通過內部類實例實現. innerClass.printInner(); } }
編譯後生成OuterClass.class和OuterClass$InnerClass.class兩個Class文件。
對進行反編譯得到:
class OuterClass$InnerClass { // Field descriptor #6 Ljava/lang/String; private java.lang.String strInner; // Field descriptor #8 LOuterClass; final synthetic OuterClass this$0; //指向外部類對象的指針,編譯器會默認為成員內部類創建指向外部類的引用 // Method descriptor #10 (LOuterClass;)V // Stack: 2, Locals: 2 //OuterClass$InnerClass構造器,雖然Java源碼中內部類的構造器是一個無參構造器,但編譯器會默認添加一個指向外部類引用的參數,賦值給this$0變量. //這就是為什麽前文提到的只有先創建外部類實例才能創建成員內部類實例、成員內部類可以隨意訪問外部類的成員變量和方法。 OuterClass$InnerClass(OuterClass arg0); 0 aload_0 [this] 1 aload_1 [arg0] 2 putfield OuterClass$InnerClass.this$0 : OuterClass [12] 5 aload_0 [this] 6 invokespecial java.lang.Object() [14] 9 return Line numbers: [pc: 0, line: 15] Local variable table: [pc: 0, pc: 10] local: this index: 0 type: OuterClass.InnerClass // Method descriptor #16 ()V // Stack: 4, Locals: 1 public void printInner(); 0 getstatic java.lang.System.out : java.io.PrintStream [22] 3 new java.lang.StringBuilder [28] 6 dup 7 ldc <String "InnerClass print : "> [30] 9 invokespecial java.lang.StringBuilder(java.lang.String) [32] 12 aload_0 [this] 13 getfield OuterClass$InnerClass.this$0 : OuterClass [12] 16 invokestatic OuterClass.access$0(OuterClass) : java.lang.String [35] 19 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [41] 22 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [45] 25 invokevirtual java.io.PrintStream.println(java.lang.String) : void [49] 28 aload_0 [this] 29 getfield OuterClass$InnerClass.this$0 : OuterClass [12] 32 invokestatic OuterClass.access$1(OuterClass) : void [54] 35 return Line numbers: [pc: 0, line: 23] [pc: 28, line: 26] [pc: 35, line: 27] Local variable table: [pc: 0, pc: 36] local: this index: 0 type: OuterClass.InnerClass Inner classes: [inner class info: #1 OuterClass$InnerClass, outer class info: #36 OuterClass inner name: #60 InnerClass, accessflags: 0 default] }
由於成員內部類實例含有對外部類實例的引用,所以即便在外部類實例沒有被引用但成員內部類實例有人引用的情況下,外部類實例不會被回收。
2、局部內部類(local inner Class)
局部內部類是嵌套在方法或作用域中的內部類。與成員內部類不同的是,當且僅當局部內部類出現在非靜態的環境(如非靜態方法)中時,才會擁有對外部類實例的引用。當出現在靜態環境中,內部類實例沒有對外部類實例的引用,也不擁有外圍類任何靜態成員。
栗子:
public class OuterClass { //局部內部類要訪問外部的變量或對象必須用final修飾,非靜態方法中局部內部類Inner實例含有對外圍類OuterClass實例的引用 public void print(final String paramStr) { final String str = "world"; //局部內部類沒有訪問修飾符 class InnerClass { public void printInner() { System.out.println("InnerClass print paramStr : " + paramStr); System.out.println("InnerClass print str : " + str); } } InnerClass innerClass = new InnerClass(); innerClass.printInner(); } public static void main(String[] args) { OuterClass outerClass = new OuterClass(); outerClass.print("hello"); } }
編譯後產生的Class文件為OuterClass.class和OuterClass$1InnerClass.class,局部內部類的Class文件名比成員內部類的Class文件名多了數字1。
3、匿名內部類(Anonymous Inner Class)
顧名思義,匿名類就是沒有名字的類,它是一種特殊的局部內部類,匿名內部類沒有構造方法,在使用的同時被聲明和實例化。
常見用法:1、動態的創建函數對象。例如比較器(Comparator),策略模式。
函數對象是:如果一個對象僅僅導出執行其他對象(對象被顯示傳遞給方法)上的操作的方法,這樣的實例被稱為函數對象。
下面的實現Comparator接口的匿名類實例就是一個函數對象。以這種方式使用匿名類時,每次執行方法都會新建一個實例,如果被頻繁的調用,效率會很低。
Arrays.sort(stringArray, new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.length() - o2.length(); } });
考慮到實現Comparator接口的匿名類沒有成員變量(即它是無狀態的),把它作為一個單例(Singleton)是非常合適的。
class Host { //公共靜態常量 public static final Comparator<String> cmp = new MyCmp(); //私有靜態內部類 private static class MyCmp implements Comparator<String> { @Override public int compare(String o1, String o2) { return o1.length() - o2.length(); } } }
2、創建過程對象,例如創建Runnable,Thread或者TimerTask實例。
3、用在靜態工廠方法的內部:
栗子:
public class OuterClass { //匿名內部類要訪問外部的變量或對象必須用final修飾 public void startThread(final String paramStr) { final String str = "world"; //匿名內部類沒有訪問修飾符 Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Thread started paramStr : " + paramStr); System.out.println("Thread started : " + str); } }; Thread t = new Thread(runnable); t.start(); } public static void main(String[] args) { OuterClass outerClass = new OuterClass(); outerClass.startThread("hello"); } }
編譯後產生的Class文件為OuterClass.class和OuterClass$1.class,匿名內部類的Class文件名只有數字1。
前文提到,匿名內部類和局部內部類的訪問外部的成員變量必須用final修飾,下面以匿名內部類為例解釋一下原因:
生命周期不一致問題:paramStr和str兩個變量的生命周期僅限於startThread方法內,當startThread方法執行結束後,這兩個變量的生命周期就結束了,但另外一個線程中的run方法很可能還沒有結束,再去訪問paramStr和str變量是不可能的。
那Java怎麽解決的這個問題呢?復制
反編譯Class文件得到:
// Compiled from OuterClass.java (version 1.7 : 51.0, super bit) class OuterClass$1 implements java.lang.Runnable { // Field descriptor #8 LOuterClass; final synthetic OuterClass this$0; //指向外部類實例的引用 // Field descriptor #10 Ljava/lang/String; private final synthetic java.lang.String val$paramStr; //編譯器默認生成val$paramStr成員變量,即paramStr參數變量的拷貝 // Method descriptor #12 (LOuterClass;Ljava/lang/String;)V // Stack: 2, Locals: 3 OuterClass$1(OuterClass arg0, java.lang.String arg1); 0 aload_0 [this] 1 aload_1 [arg0] 2 putfield OuterClass$1.this$0 : OuterClass [14] 5 aload_0 [this] 6 aload_2 [arg1] 7 putfield OuterClass$1.val$paramStr : java.lang.String [16] 10 aload_0 [this] 11 invokespecial java.lang.Object() [18] 14 return Line numbers: [pc: 0, line: 1] [pc: 10, line: 7] Local variable table: [pc: 0, pc: 15] local: this index: 0 type: new OuterClass(){} // Method descriptor #20 ()V // Stack: 4, Locals: 1 public void run(); 0 getstatic java.lang.System.out : java.io.PrintStream [26] 3 new java.lang.StringBuilder [32] 6 dup 7 ldc <String "Thread started paramStr : "> [34] 9 invokespecial java.lang.StringBuilder(java.lang.String) [36] 12 aload_0 [this] 13 getfield OuterClass$1.val$paramStr : java.lang.String [16] 16 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [39] 19 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [43] 22 invokevirtual java.io.PrintStream.println(java.lang.String) : void [47] 25 getstatic java.lang.System.out : java.io.PrintStream [26] 28 ldc <String "Thread started : world"> [52] 30 invokevirtual java.io.PrintStream.println(java.lang.String) : void [47] 33 return Line numbers: [pc: 0, line: 10] [pc: 25, line: 11] [pc: 33, line: 12] Local variable table: [pc: 0, pc: 34] local: this index: 0 type: new OuterClass(){} Inner classes: [inner class info: #1 OuterClass$1, outer class info: #0 inner name: #0, accessflags: 0 default] Enclosing Method: #57 #59 OuterClass.startThread(Ljava/lang/String;)V }
可以看到,編譯器默認為匿名內部類創建了兩個成員變量:this$0指向外部類的引用;val$paramStr為String變量。構造函數中用startThread方法的形參paramStr初始化val$paramStr變量,即val$paramStr是方法形參paramStr的一個拷貝。也就是說,run方法訪問的是paramStr的拷貝,所以即便paramStr生命周期結束也不會影響run方法的執行,解決了生命周期不一致問題。那paramStr為什麽要用final修飾呢?假如paramStr是一個非final普通變量,那就可以在內部類中修改val$paramStr變量的值,但paramStr的值不會受影響,造成數據不一致問題,所以把paramStr聲明為final變量,不允許修改。
對於局部變量str,被final修飾意味著str是一個常量,在編譯期間就可以確定並放入常量池,編譯器默認為內部類創建一個局部變量的拷貝,通過拷貝去常量池訪問就可以了,看這條語句ldc <String "Thread started : world"> [52] 表示將字符串變量壓入棧頂。(對常量池的概念理解不夠深入)。
總的來說,如果局部變量的值在編譯期間就可以確定(str),則直接在匿名內部類(局部內部類)中創建一份拷貝;如果局部變量的值無法在編譯期間確定(paramStr),則通過構造器傳參的方式對拷貝進行初始化。由於被final修飾的變量不能被修改,保證拷貝和原始變量的一致,給人的感覺好像是變量的生命周期延長了,引出了Java中的閉包。
閉包是什麽東東?
4、靜態內部類(static inner class)
靜態內部類是最簡單的一種內部類,可以把靜態內部類看成是普通的類,恰好被定義在另一個類內部。它不依賴於外圍類實例,可以在外圍類實例之外獨立存在。
常見用法:作為公有的輔助類,僅當它與外圍類一起使用時才有意義。
Map中Entry為私有靜態內部類,Entry是外部類的一個組件。雖然每個Entry都與一個Map相關聯,但entry上的方法(getValue和getKey)不需要訪問Map,所以沒必要使用非靜態成員內部類。因為非靜態成員內部類的實例都包含指向外圍類實例的引用,即每個Entry都含有一個指向Map的引用,造成空間和時間的浪費。
栗子:
public class OuterClass { private String str = "hello"; private static String staticStr = "static_hello"; private static void print() { System.out.println("OuterClass print : " + staticStr); } /*靜態內部類*/ static class InnerClass { public static String staticStrInner = "static_hello_Inner"; public void printInner() { //靜態內部類只能訪問外部類的靜態成員和靜態方法,不能訪問外部類的非靜態成員和方法。 System.out.println("InnerClass print : " + staticStr); print(); } } public static void main(String[] args) { //靜態內部類實例的創建不依賴外部類 InnerClass innerClass = new InnerClass(); innerClass.printInner(); } }
為什麽要使用內部類?
1、解決多繼承問題:Java不支持多繼承,不管外部類有沒有繼承類,成員內部類都可以獨立的繼承某個類,而成員內部類又可以訪問外部類,相當於實現多繼承了。
2、對於只使用一次的類,在其他地方不會使用這個類,那麽聲明一個外部類就沒有必要了,使用局部內部類和成員內部類就可以。
3、內部類可以實現更好的封裝,使類與類之間的關系更加緊密。
如何選擇使用哪種內部類?
1、如果成員內部類的每個實例都需要一個指向其外圍類的引用,選擇非靜態成員內部類,否則選擇靜態成員內部類。
2、假設內部類在一個方法的內部,在方法之外不需要使用,如果只需要在一個地方創建實例且已經有了一個預置的類型可以說明這個類的特征,就要把它做成匿名內部類,否則選擇局部內部類。
參考資料:
1、(Java語法糖4:內部類)http://www.cnblogs.com/xrq730/p/4875907.html
2、(從反編譯認識內部類)http://blog.csdn.net/le_le_name/article/details/52338096
3、(為什麽必須是final的呢?)http://cuipengfei.me/blog/2013/06/22/why-does-it-have-to-be-final/
4、(Java語法糖系列五:內部類和閉包)http://www.jianshu.com/p/f55b11a4cec2
5、http://www.cnblogs.com/chenssy/p/3388487.html(java提高篇(八)----詳解內部類)
6、( java提高篇(九)-----詳解匿名內部類)http://blog.csdn.net/chenssy/article/details/13170015
Java語法糖(二)