昨天有一個比較愛思考的同事和我提起一個問題:為什麼匿名內部類使用的局部變量和參數需要final修飾,而外部類的成員變量則不用?對這個問題我一直作為默認的語法了,木有仔細想過為什麼(在分析完後有點印象在哪本書上看到過,但是就是沒有找到,難道是我的幻覺?呵呵)。雖然沒有想過,但是還是借著之前研究過字節碼的基礎上,分析了一些,感覺上是找到了一些答案,分享一下;也希望有大牛給指出一些不足的地方。
假如我們有以下的代碼:
- interface Printer {
- public void print();
- }
- class MyApplication {
- private int fIEld = 10;
- public void print(final Integer param) {
- final long local = 100;
- final long local2 = param.longValue() + 100;
- Printer printer = new Printer() {
- @Override
- public void print() {
- System.out.println("Local value: " + local);
- System.out.println("Local2 value: " + local2);
- System.out.println("Parameter: " + param);
- System.out.println("FIEld value: " + fIEld);
- }
- };
- printer.print();
- }
- }
這裡因為param要在匿名內部類的print()方法中使用,因而它要用final修飾;local/local2是局部變量,因而也需要final修飾;而fIEld是外部類MyApplication的字段,因而不需要final修飾。這種設計是基於什麼理由呢?
我想這個問題應該從Java是如何實現匿名內部類的。其中有兩點:
1、匿名內部類可以使用外部類的變量(局部或成員變來那個)。
2、匿名內部類中不同的方法可以共享這些變量。
根據這兩點信息我們就可以分析,可能這些變量會在匿名內部類的字段中保存著,並且在構造的時候將他們的值/引用傳入內部類。這樣就可以保證同時實現上述兩點了。
事實上,Java就是這樣設計的,並且所謂匿名類,其實並不是匿名的,只是編譯器幫我們命名了而已。這點我們可以通過這兩個類編譯出來的字節碼看出來:
- // Compiled from Printer.Java (version 1.6 : 50.0, super bit)
- class levin.test.anonymous.MyApplication$1 implements levin.test.anonymous.Printer {
- // FIEld descriptor #8 Llevin/test/anonymous/MyApplication;
- final synthetic levin.test.anonymous.MyApplication this$0;
- // FIEld descriptor #10 J
- private final synthetic long val$local2;
- // FIEld descriptor #12 LJava/lang/Integer;
- private final synthetic Java.lang.Integer val$param;
- // Method descriptor #14 (Llevin/test/anonymous/MyApplication;JLJava/lang/Integer;)V
- // Stack: 3, Locals: 5
- MyApplication$1(levin.test.anonymous.MyApplication arg0, long arg1, Java.lang.Integer arg2);
- 0 aload_0 [this]
- 1 aload_1 [arg0]
- 2 putfIEld levin.test.anonymous.MyApplication$1.this$0 : levin.test.anonymous.MyApplication [16]
- 5 aload_0 [this]
- 6 lload_2 [arg1]
- 7 putfIEld levin.test.anonymous.MyApplication$1.val$local2 : long [18]
- 10 aload_0 [this]
- 11 aload 4 [arg2]
- 13 putfIEld levin.test.anonymous.MyApplication$1.val$param : Java.lang.Integer [20]
- 16 aload_0 [this]
- 17 invokespecial Java.lang.Object() [22]
- 20 return
- Line numbers:
- [pc: 0, line: 1]
- [pc: 16, line: 13]
- Local variable table:
- [pc: 0, pc: 21] local: this index: 0 type: new levin.test.anonymous.MyApplication(){}
- // Method descriptor #24 ()V
- // Stack: 4, Locals: 1
- public void print();
- 0 getstatic java.lang.System.out : Java.io.PrintStream [30]
- 3 ldc <String "Local value: 100"> [36]
- 5 invokevirtual java.io.PrintStream.println(Java.lang.String) : void [38]
- 8 getstatic java.lang.System.out : Java.io.PrintStream [30]
- 11 new Java.lang.StringBuilder [44]
- 14 dup
- 15 ldc <String "Local2 value: "> [46]
- 17 invokespecial java.lang.StringBuilder(Java.lang.String) [48]
- 20 aload_0 [this]
- 21 getfIEld levin.test.anonymous.MyApplication$1.val$local2 : long [18]
- 24 invokevirtual Java.lang.StringBuilder.append(long) : Java.lang.StringBuilder [50]
- 27 invokevirtual java.lang.StringBuilder.toString() : Java.lang.String [54]
- 30 invokevirtual java.io.PrintStream.println(Java.lang.String) : void [38]
- 33 getstatic java.lang.System.out : Java.io.PrintStream [30]
- 36 new Java.lang.StringBuilder [44]
- 39 dup
- 40 ldc <String "Parameter: "> [58]
- 42 invokespecial java.lang.StringBuilder(Java.lang.String) [48]
- 45 aload_0 [this]
- 46 getfIEld levin.test.anonymous.MyApplication$1.val$param : Java.lang.Integer [20]
- 49 invokevirtual java.lang.StringBuilder.append(java.lang.Object) : Java.lang.StringBuilder [60]
- 52 invokevirtual java.lang.StringBuilder.toString() : Java.lang.String [54]
- 55 invokevirtual java.io.PrintStream.println(Java.lang.String) : void [38]
- 58 getstatic java.lang.System.out : Java.io.PrintStream [30]
- 61 new Java.lang.StringBuilder [44]
- 64 dup
- 65 ldc <String "FIEld value: "> [63]
- 67 invokespecial java.lang.StringBuilder(Java.lang.String) [48]
- 70 aload_0 [this]
- 71 getfIEld levin.test.anonymous.MyApplication$1.this$0 : levin.test.anonymous.MyApplication [16]
- 74 invokestatic levin.test.anonymous.MyApplication.Access$0(levin.test.anonymous.MyApplication) : int [65]
- 77 invokevirtual Java.lang.StringBuilder.append(int) : Java.lang.StringBuilder [71]
- 80 invokevirtual java.lang.StringBuilder.toString() : Java.lang.String [54]
- 83 invokevirtual java.io.PrintStream.println(Java.lang.String) : void [38]
- 86 return
- Line numbers:
- [pc: 0, line: 16]
- [pc: 8, line: 17]
- [pc: 33, line: 18]
- [pc: 58, line: 19]
- [pc: 86, line: 20]
- Local variable table:
- [pc: 0, pc: 87] local: this index: 0 type: new levin.test.anonymous.MyApplication(){}
- Inner classes:
- [inner class info: #1 levin/test/anonymous/MyApplication$1, outer class info: #0
- inner name: #0, Accessflags: 0 default]
- Enclosing Method: #66 #77 levin/test/anonymous/MyApplication.print(LJava/lang/Integer;)V
- }
- // Compiled from Printer.Java (version 1.6 : 50.0, super bit)
- class levin.test.anonymous.MyApplication {
- // FIEld descriptor #6 I
- private int fIEld;
- // Method descriptor #8 ()V
- // Stack: 2, Locals: 1
- MyApplication();
- 0 aload_0 [this]
- 1 invokespecial Java.lang.Object() [10]
- 4 aload_0 [this]
- 5 bipush 10
- 7 putfield levin.test.anonymous.MyApplication.fIEld : int [12]
- 10 return
- Line numbers:
- [pc: 0, line: 7]
- [pc: 4, line: 8]
- [pc: 10, line: 7]
- Local variable table:
- [pc: 0, pc: 11] local: this index: 0 type: levin.test.anonymous.MyApplication
- // Method descriptor #19 (LJava/lang/Integer;)V
- // Stack: 6, Locals: 7
- public void print(Java.lang.Integer param);
- 0 ldc2_w <Long 100> [20]
- 3 lstore_2 [local]
- 4 aload_1 [param]
- 5 invokevirtual Java.lang.Integer.longValue() : long [22]
- 8 ldc2_w <Long 100> [20]
- 11 ladd
- 12 lstore 4 [local2]
- 14 new levin.test.anonymous.MyApplication$1 [28]
- 17 dup
- 18 aload_0 [this]
- 19 lload 4 [local2]
- 21 aload_1 [param]
- 22 invokespecial levin.test.anonymous.MyApplication$1(levin.test.anonymous.MyApplication, long, Java.lang.Integer) [30]
- 25 astore 6 [printer]
- 27 aload 6 [printer]
- 29 invokeinterface levin.test.anonymous.Printer.print() : void [33] [nargs: 1]
- 34 return
- Line numbers:
- [pc: 0, line: 11]
- [pc: 4, line: 12]
- [pc: 14, line: 13]
- [pc: 27, line: 22]
- [pc: 34, line: 23]
- Local variable table:
- [pc: 0, pc: 35] local: this index: 0 type: levin.test.anonymous.MyApplication
- [pc: 0, pc: 35] local: param index: 1 type: Java.lang.Integer
- [pc: 4, pc: 35] local: local index: 2 type: long
- [pc: 14, pc: 35] local: local2 index: 4 type: long
- [pc: 27, pc: 35] local: printer index: 6 type: levin.test.anonymous.Printer
- // Method descriptor #45 (Llevin/test/anonymous/MyApplication;)I
- // Stack: 1, Locals: 1
- static synthetic int Access$0(levin.test.anonymous.MyApplication arg0);
- 0 aload_0 [arg0]
- 1 getfield levin.test.anonymous.MyApplication.fIEld : int [12]
- 4 ireturn
- Line numbers:
- [pc: 0, line: 8]
- Inner classes:
- [inner class info: #28 levin/test/anonymous/MyApplication$1, outer class info: #0
- inner name: #0, Accessflags: 0 default]
- }
從這兩段字節碼中可以看出,編譯器為我們的匿名類起了一個叫MyApplication$1的名字,它包含了三個final字段(這裡synthetic修飾符是指這些字段是由編譯器生成的,它們並不存在於源代碼中):
MyApplication的應用this$0
long值val$local2
Integer引用val$param
這些字段在構造函數中賦值,而構造函數則是在MyApplication.print()方法中調用。
由此,我們可以得出一個結論:Java對匿名內部類的實現是通過編譯器來支持的,即通過編譯器幫我們產生一個匿名類的類名,將所有在匿名類中用到的局部變量和參數做為內部類的final字段,同是內部類還會引用外部類的實例。其實這裡少了local的變量,這是因為local是編譯器常量,編譯器對它做了替換的優化。
其實Java中很多語法都是通過編譯器來支持的,而在虛擬機/字節碼上並沒有什麼區別,比如這裡的final關鍵字,其實細心的人會發現在字節碼中,param參數並沒有final修飾,而final本身的很多實現就是由編譯器支持的。類似的還有Java中得泛型和逆變、協變等。這是題外話。
有了這個基礎後,我們就可以來分析為什麼有些要用final修飾,有些卻不用的問題。
首先我們來分析local2變量,在”匿名類”中,它是通過構造函數傳入到”匿名類”字段中的,因為它是基本類型,因而在夠著函數中賦值時(撇開對函數參數傳遞不同虛擬機的不同實現而產生的不同效果),它事實上只是值的拷貝;因而加入我們可以在”匿名類”中得print()方法中對它賦值,那麼這個賦值對外部類中得local2變量不會有影響,而程序員在讀代碼中,是從上往下讀的,所以很容易誤認為這段代碼賦值會對外部類中得local2變量本身產生影響,何況在源碼中他們的名字都是一樣的,所以我認為了避免這種confuse導致的一些問題,Java設計者才設計出了這樣的語法。
對引用類型,其實也是一樣的,因為引用的傳遞事實上也只是傳遞引用的數值(簡單的可以理解成為地址),因而對param,如果可以在”匿名類”中賦值,也不會在外部類的print()後續方法產生影響。雖然這樣,我們還是可以在內部類中改變引用內部的值的,如果引用類型不是只讀類型的話;在這裡Integer是只讀類型,因而我們沒法這樣做。(如果學過C++的童鞋可以想想常量指針和指針常量的區別)。
現在還剩下最後一個問題:為什麼引用外部類的字段卻是可以不用final修飾的呢?細心的童鞋可能也已經發現答案了,因為內部類保存了外部類的引用,因而內部類中對任何字段的修改都回真實的反應到外部類實例本身上,所以不需要用final來修飾它。
這個問題基本上就分析到這裡了,不知道我有沒有表達清楚了。
加點題外話吧。
首先是,對這裡的字節碼,其實還有一點可以借鑒的地方,就是內部類在使用外部類的字段時不是直接取值,而是通過編譯器在外部類中生成的靜態的Access$0()方法來取值,我的理解,這裡Java設計者想盡量避免其他類直接訪問一個類的數據成員,同時生成的Access$0()方法還可以被其他類所使用,這遵循了面向對象設計中的兩個重要原則:封裝和復用。
另外,對這個問題也讓我意識到了即使是語言語法層面上的設計都是有原因可循的,我們要善於多問一些為什麼,理解這些設計的原因和局限,記得曾聽到過一句話:知道一門技術的局限,我們才能很好的理解這門技術可以用來做什麼。也只有這樣我們才能不斷的提高自己。在解決了這個問題後,我突然冒出了一句說Java這樣設計也是合理的。是啊,語法其實就一幫人創建的一種解決某些問題的方案,當然有合理和不合理之分,我們其實不用對它視若神聖。
之前有進過某著名高校的研究生群,即使在那裡,碼農論也是甚囂塵上,其實碼農不碼農並不是因為程序員這個職位引起的,而是個人引起的,我們要不斷理解代碼內部的本質才能避免一直做碼農的命運那。個人愚見而已,呵呵。