語法糖
接下來幾篇文章要開啟一個Java語法糖系列,所以首先講講什麼是語法糖。語法糖是一種幾乎每種語言或多或少都提供過的一些方便程式設計師開發程式碼的語法,它只是編譯器實現的一些小把戲罷了,編譯期間以特定的位元組碼或者特定的方式對這些語法做一些處理,開發者就可以直接方便地使用了。這些語法糖雖然不會提供實質性的功能改進,但是它們或能提高效能、或能提升語法的嚴謹性、或能減少編碼出錯的機會。Java提供給了使用者大量的語法糖,比如泛型、自動裝箱、自動拆箱、foreach迴圈、變長引數、內部類、列舉類、斷言(assert)等。
可變長度引數
先講可變長度引數,看一段程式碼:
public static void main(String[] args) { print("000", "111", "222", "333"); } public static void print(String... strs) { for (int i = 0; i < strs.length; i++) { System.out.println(strs[i]); } }
print方法的引數的意思是表示傳入的String個數是不定的,看一下程式碼的執行結果:
000 111 222 333
我用陣列遍歷的方式成功地將輸入的引數遍歷出來了,這說明兩個問題:
1、可以使用遍歷陣列的方式去遍歷可變引數
2、可變引數是利用陣列實現的
既然這樣,那我其實main函式也可以這麼寫,完全可以:
String[] strs = {"000", "111", "222", "333"};
print(strs);
那直接傳入一個陣列不就好了?問題是,陣列是要指定長度的,萬一這次我想傳2個String,下次我想傳3個String怎麼辦呢?
最後,注意一點,可變長度引數必須作為方法引數列表中的的最後一個引數且方法引數列表中只能有一個可變長度引數。
foreach迴圈原理
以前對foreach迴圈就是這麼用著,觸動我去研究foreach迴圈的原理的原因是大概兩個月前,自己寫了一個ArrayList,想用foreach迴圈遍歷一下看一下寫的效果,結果報了空指標異常。本文就寫寫foreach迴圈的原理,先看一下這麼一段程式碼:
public static void main(String[] args) { List<String> list = new ArrayList<String>(); list.add("111"); list.add("222"); for (String str : list) { System.out.println(str); } }
用foreach迴圈去遍歷這個list,結果就不說了,都知道。看一下Java是如何處理這個foreach迴圈的,javap反編譯一下:
F:\程式碼\MyEclipse\TestArticle\bin\com\xrq\test21>javap -verbose TestMain.class
反編譯出來的內容很多,有類資訊、符號引用、位元組碼資訊,擷取一段資訊:
1 public static void main(java.lang.String[]); 2 flags: ACC_PUBLIC, ACC_STATIC 3 Code: 4 stack=2, locals=4, args_size=1 5 0: new #16 // class java/util/ArrayList 6 3: dup 7 4: invokespecial #18 // Method java/util/ArrayList."<in 8 it>":()V 9 7: astore_1 10 8: aload_1 11 9: ldc #19 // String 111 12 11: invokeinterface #21, 2 // InterfaceMethod java/util/List. 13 add:(Ljava/lang/Object;)Z 14 16: pop 15 17: aload_1 16 18: ldc #27 // String 222 17 20: invokeinterface #21, 2 // InterfaceMethod java/util/List. 18 add:(Ljava/lang/Object;)Z 19 25: pop 20 26: aload_1 21 27: invokeinterface #29, 1 // InterfaceMethod java/util/List. 22 iterator:()Ljava/util/Iterator;
看不懂沒關係,new、dup、invokespecial這些本來就是位元組碼指令表內定義的指令,虛擬機器會根據這些指令去執行指定的C++程式碼,完成每個指令的功能。關鍵看到21、22這兩行就可以了,看到了一個iterator,所以得出結論:在編譯的時候編譯器會自動將對for這個關鍵字的使用轉化為對目標的迭代器的使用,這就是foreach迴圈的原理。進而,我們再得出兩個結論:
1、ArrayList之所以能使用foreach迴圈遍歷,是因為ArrayList所有的List都是Collection的子介面,而Collection是Iterable的子介面,ArrayList的父類AbstractList正確地實現了Iterable介面的iterator方法。之前我自己寫的ArrayList用foreach迴圈直接報空指標異常是因為我自己寫的ArrayList並沒有實現Iterable介面
2、任何一個集合,無論是JDK提供的還是自己寫的,只要想使用foreach迴圈遍歷,就必須正確地實現Iterable介面
實際上,這種做法就是23中設計模式中的迭代器模式。
陣列呢?
上面的講完了,好理解,但是不知道大家有沒有疑問,至少我是有一個疑問的:陣列並沒有實現Iterable介面啊,為什麼陣列也可以用foreach迴圈遍歷呢?先給一段程式碼,再反編譯:
public static void main(String[] args) { int[] ints = {1,2,3,4,5}; for (int i : ints) System.out.println(i); }
同樣反編譯一下,看一下關鍵的資訊:
1 0: iconst_2 2 1: newarray int 3 3: dup 4 4: iconst_0 5 5: iconst_1 6 6: iastore 7 7: dup 8 8: iconst_1 9 9: iconst_2 10 10: iastore 11 11: astore_1 12 12: aload_1 13 13: dup 14 14: astore 5 15 16: arraylength 16 17: istore 4 17 19: iconst_0 18 20: istore_3 19 21: goto 39 20 24: aload 5 21 26: iload_3 22 27: iaload 23 28: istore_2 24 29: getstatic #16 // Field java/lang/System.out:Ljav 25 a/io/PrintStream; 26 32: iload_2 27 33: invokevirtual #22 // Method java/io/PrintStream.prin 28 tln:(I)V 29 36: iinc 3, 1 30 39: iload_3 31 40: iload 4 32 42: if_icmplt 24 33 45: return
這是完整的這段main函式對應的45個位元組碼指令,因為這涉及一些壓棧、出棧、推送等一些計算機原理性的內容且對於這些位元組碼指令的知識的理解需要一些C++的知識,所以就不解釋了。簡單對照位元組碼指令表之後,我個人對於這45個位元組碼的理解是Java將對於陣列的foreach迴圈轉換為對於這個陣列每一個的迴圈引用。