語法糖
接觸語法糖是在讀《深入理解Java虛擬機》的時候,初始覺得語法糖是個挺有意思的概念,遂抽出一周實踐詳細總結一下語法糖。百度百科對於語法糖的解釋如下;
語法糖(Syntactic sugar),也譯為糖衣語法,是由英國計算機科學家彼得·約翰·蘭達(Peter J. Landin)發明的一個術語,指計算機語言中添加的某種語法,這種語法對語言的功能並沒有影響,但是更方便程序員使用。通常來說使用語法糖能夠增加程序的可讀性,從而減少程序代碼出錯的機會。
通過上面的解釋我們知道語法糖可以看做是編譯器實現的一些“小把戲”,這些“小把戲”可以提高編碼效率,提升語法的嚴謹性,減少編碼出錯的機會,但是並不能對代碼性能提供實質性的幫助。下面我們對Java中常見的幾種語法糖進行總結分析;
1 泛型與泛型擦除
了解泛型的Java開發者都知道,泛型是JDK1.5新增的特性,本質是對參數化類型的應用,也就是說“所操作的數據類型被指定為一個參數”。這種參數化類型可以用在類、方法、接口的創建中。分別稱為泛型類、泛型方法、泛型接口。
泛型早在C++中就已經存在了,JDK1.5之前,Java語言還沒有出現泛型,只能通過Object是所有類型的父類和類型強制轉換兩個特點來實現類型泛型,如下代碼:
@Test public void test1(){ List list1 = new ArrayList();//如果沒有泛型的話,那么list中我們任意的存放各種數據類型的 數據 list1.add("String"); list1.add(new ClassCast()); Object object1 = list1.get(0);//那么從集合中取出數據的時候,只能通過向上轉型為所有類的父類Object來取出數據 String str = (String) object1;//然后通過強制轉換來獲取正確數據類型的數據,這里是很容易出現類型轉換異常的 // String str1 = (String) list1.get(1);//出現ClassCastException System.out.println(str);//String }
我們可以看到,如果沒有泛型的話,那么我們在取出數據的時候向下轉型成什么數據類型都是可以的,只有程序員和運行時的虛擬機知道這個取出的Object到底是什么類型的對象。在編譯期間,編譯器無法檢測這個Object是否轉型成功,如果僅僅依賴程序員去保證這項操作的正確性,許多ClassCastException的風險就會轉嫁到程序運行期之中。
Java源自C,那么Java中的泛型和C中的泛型是一樣的嗎?實際上是大相徑庭的,這中間就牽涉到了幾個概念:真實泛型和偽泛型;
C#里面的泛型無論是在程序源代碼中、編譯后的IL(類似於.class的中間語言,此時的泛型就是一個占位符)或是運行期的CLR中,都是切實存在的,List<int>與List<String>就是兩個不同的類型,它們在系統運行期生成,有自己的需方法表和類型數據,這種實現稱為類型膨脹,基於這種方法實現的泛型稱為真實泛型。說白了真實泛型不管是在源代碼還是機器碼中都是能夠起到約束作用的。
Java語言中的泛型則與C#的截然不同,它只會存在於程序源碼中(即只能在.java文件中看到泛型的存在),而在編譯后的字節碼文件中,就已經看不到泛型的痕跡了,此時已經替換為原來的原生類型(Raw Type,也稱為裸類型)了,並且在相應的地方插入類強制類型轉換的代碼,因此,對於運行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類,所以泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱為類型擦除,基於這種方法實現的泛型稱為偽泛型。下面我們來看一段代碼,並且比對一下編譯后的代碼可以看出泛型擦除的痕跡;
源代碼 @Test public void test2(){ List<String> list1 = new ArrayList();//如果沒有泛型的話,那么list中我們任意的存放各種數據類型的 數據 list1.add("String"); // list1.add(new ClassCast());此時編譯器提示List<String>不能接受該參數 /** * 那么從集合中取出數據的時候,源代碼中看起來好像是取出來的就是我們存放 * 的數據類型,實際上JVM中的操作還是取出來是Object類型, * 然后通過強制轉換轉為String,只是編譯器通過泛型幫我們進行了類型檢查, * 所以將運行時異常轉變為編譯期異常了 */ String str1 = list1.get(0); System.out.println(str1);//String }
編譯后的代碼 @Test public void test2() { List list1 = new ArrayList(); list1.add("String"); String str1 = (String)list1.get(0); System.out.println(str1); }
通過上面的對比我們發現Java中的泛型只是在編譯期幫助我們進行類型檢查的,通過編譯器的類型檢查將運行期異常轉變為編譯器錯誤。那么問自己一個問題,泛型是不是真的就被擦除的一點痕跡都不剩了呢?實際上也倒並非完全如此;
由於Java泛型的引入,各種場景(虛擬機解析、反射等)下的方法調用都可能對原有的基礎產生影響和新的需求,如在泛型類中如何獲取傳入的參數化類型等。因此,JCP組織對虛擬機規范做出了相應的修改,引入了諸如Signature、LocalVariableTypeTable等新的屬性用於解決伴隨泛型而來的參數類型的識別問題,Signature是其中最重要的一項屬性,它的作用就是存儲一個方法在字節碼層面的特征簽名,這個屬性中保存的參數類型並不是原生類型,而是包括了參數化類型的信息。修改后的虛擬機規范要求所有能識別49.0以上版本的Class文件的虛擬機都要能正確地識別Signature參數。總之來說,擦除法所謂的擦除,僅僅是對方法的Code屬性中的字節碼進行擦除,實際上元數據中還是保留了泛型信息,這也是我們在通過反射手段獲取參數化類型的根本依據【下面我們綴一段通過反射獲取泛型參數類型的操作】。聊到這里,考慮一個問題,那就是泛型能作為重載的依據嗎?下面我們了解一下;
package cn.syntacticSugar.genericity.before; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Arrays; import java.util.List; import java.util.ArrayList; public class GenericDemo { public static void main(String[] args) throws NoSuchMethodException, SecurityException{ // test(new ArrayList<String>()); // GenericDemo gd = new GenericDemo(); // Class<GenericDemo> clazz = (Class<GenericDemo>) gd.getClass(); // System.out.println(clazz.getName()); // Method[] method = clazz.getMethods(); // for (int i = 0; i < method.length; i++) { // Class<?> returnType = method[i].getReturnType(); // System.out.print("方法名稱"+method[i].getName()+" "); // System.out.print("方法返回參數:"+returnType.getName()+" "); // Class<Parameter>[] parameter = (Class<Parameter>[]) method[i].getParameterTypes(); // for (int j = 0; j < parameter.length; j++) { // System.out.println(parameter[j].getName()); // } // // } Method method = GenericDemo.class.getMethod("test", List.class); Type[] types = method.getGenericParameterTypes(); for (Type type : types) { System.out.println("#"+type); if(type instanceof ParameterizedType){ Type[] genericTypes = ((ParameterizedType)type).getActualTypeArguments(); for(Type genericType:genericTypes){ System.out.println("泛型類型"+genericType); } } } // Type returnType = method.getGenericReturnType();//獲取返回類型的泛型 // if(returnType instanceof ParameterizedType){ // Type [] genericTypes2s =((ParameterizedType)returnType).getActualTypeArguments(); // for(Type genericType2:genericTypes2s){ // System.out.println("返回值,泛型類型"+genericType2); // } // } } public static String test(List<String> list){ System.out.println("invoke test(List<String> list)"); return "123"; } }
我們獲取到的泛型類型和實際類型一致,這說明的確我們的泛型並沒有完全的在JVM中消失蹤跡.
泛型和重載的一次有意思的探索
我們知道方法重載要求方法具備不同的特征簽名,而返回值並不包含在特征簽名中(在編譯層面的特征簽名),所以返回值不參與重載選擇,但是在Class文件格式之中,只要描述符不是完全一致的兩個方法就可以共存。也就是說,兩個方法如果有相同的名稱和特征簽名,但返回值不同,那它們也是可以合法地共存於一個Class文件中的。如下的代碼,我們通過三個版本的JDK進行測試,測試結果分別羅列出來
import java.util.List; import java.util.ArrayList; public class GenericDemo { public static void main(String[] args){ test(new ArrayList<String>()); test(new ArrayList<Integer>()); } public static String test(List<String> list){ System.out.println("invoke test(List<String> list)"); return "123"; } public static int test(List<Integer> list){ System.out.println("invoke test(List<Integer> list)"); return 12; } }
JDK1.6測試:
JDK1.7測試:
JDK1.8測試
我們通過三個版本的測試,發現只有JDK1.6可以編譯文件,而且正常運行,其它再高版本的JDK不能支持這種情況下的“重載”了,但是可以將JDK1.6編譯后的.Class文件正常解釋執行.所以盡管在JVM層面重載會將返回值作為參考因素,但是在編譯層面重載只考慮方法參數,而泛型擦除使得所有的參數變成了相同的數據類型,所以如上代碼是不能通過的,而不能通過編譯的代碼,一切都是無謂的,在這里我們要注意不同的參數化類型不能作為方法重載的參考因素.
2 自動拆裝箱
除了上面說的泛型外,其實還有一種語法糖是我們很常見的,那就是自動拆裝箱。
自動拆裝箱也是JDK1.5出現的特性(所以說JDK1.5是Java的一大飛躍,之后就是JDK1.8了,這是后話了)。在自動拆裝箱之前,如果int類型想要轉換為Integer只能通過手動調用Integer.valueOf(int)來完成轉換,同樣的拆箱的時候通過Integer.intValue()來完成,這些都需要我們手動來完成,無益於代碼效率的提升。在JDK1.5中新增了自動拆裝箱特性,該特性使得我們不用再自己手動完成自動拆裝箱的操作,而是通過編譯器幫我們實現自動拆裝箱,這於性能無益,但是卻提高了我們的開發效率,屬於一個標准的語法糖。下面我們通過一段代碼仔細瞜一眼;
package cn.syntacticSugar.box; import java.util.ArrayList; import java.util.List; public class SugarBox { public static void main(String[] args) { int i = 9; Integer in = 90; List<Integer> list = new ArrayList<Integer>(); list.add(7);//自動裝箱 list.add(9); i = in + list.get(0);//自動拆箱 System.out.println(i);//97 } }
public class SugarBox { public SugarBox() { } public static void main(String args[]) { int i = 9; Integer in = Integer.valueOf(90); List list = new ArrayList(); list.add(Integer.valueOf(7)); list.add(Integer.valueOf(9)); i = in.intValue() + ((Integer)list.get(0)).intValue(); System.out.println(i); } }
關於自動拆裝箱這個語法糖我們就介紹到這里,詳細的自動拆裝箱可以參見自動拆裝箱筆記;
3 加強for循環
在我們開發編程中for循環是一種使用頻率十分高的流程控制語句,我們都知道for循環有兩種操作,一種是普通for循環,一種是JDK1.5新增的特性foreach(),也叫加強for循環;Java中的加強for循環實際上也是一種語法糖,底層是通過迭代器完成的,下面通過一段代碼來說明;
package cn.syntacticSugar.fou; import java.util.ArrayList; import java.util.List; public class SugarFor { public static void main(String[] args) { List<String> list = new ArrayList<String>(); list.add("1"); list.add("12"); list.add("123"); list.add("1234"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } System.out.println("--------------------"); for (String string : list) { System.out.println(string); } } }
package cn.syntacticSugar.fou; import java.io.PrintStream; import java.util.*; public class SugarFor { public SugarFor() { } public static void main(String args[]) { List list = new ArrayList(); list.add("1"); list.add("12"); list.add("123"); list.add("1234"); for(int i = 0; i < list.size(); i++) System.out.println((String)list.get(i)); System.out.println("--------------------"); String string; for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println(string)) string = (String)iterator.next(); } }
從上面我們可以看出普通for循環的底層仍舊是普通for循環。而加強for循環的底層是通過迭代器來進行循環操作的;通過上面的操作我們還可以看出如果條件語句的控制語句只有一條的時候,大括號“{}”是可以省略的,如果沒有省略,那么編譯器會替我們省略,這也可以理解為語法糖的一種;關於加強for我們就認識到這里,詳細認識for語句,請參考JDK簡史;
4 條件編譯
條件語句也是我們日常開發中使用頻率很高的一種語句。條件語句的判斷依據就是條件判斷的真假,一般情況下的條件判斷是動態的,需要在JVM運行時才能確認真假,但是不排除有一些條件判斷是可以在編譯器期間就能夠有結果的。這種情況下編譯器會優化代碼,將條件進行條件編譯,生成的中間碼class文件中直接以結果呈現,下面通過一段代碼來看一下;
package cn.syntacticSugar.condition; public class SugarCondition { public static void main(String[] args) { if(5>3){ System.out.println(5>3); }else { System.out.println(5<3); } } }
package cn.syntacticSugar.condition; import java.io.PrintStream; public class SugarCondition { public SugarCondition() { } public static void main(String args[]) { System.out.println(true); } }
通過上面我們可以看到,當條件在編譯期間就可以確定的話,那么編譯器會在編譯時對其優化,同時我們要注意在實際開發中IDE是會對這種代碼提示Dead Code的,意思是無效代碼的意思;條件語句也是可以用在while語句中的,那么當條件語句中用在while中會怎么樣呢?下面我們通過一些代碼看一下;
package cn.syntacticSugar.condition; public class SugarCondition { public static void main(String[] args) { test(5, 4); test11(); //test111(); } public static void test(int i1,int i2){ while(i1 > i2){ System.out.println(i1 + i2); } } public static void test11(){ while (5>3) { System.out.println(5>3); } } }
package cn.syntacticSugar.condition; import java.io.PrintStream; public class SugarCondition { public SugarCondition() { } public static void main(String args[]) { test(5, 4); test11(); } public static void test(int i1, int i2) { while(i1 > i2) System.out.println(i1 + i2); } public static void test11() { do System.out.println(true); while(true); } }
通過上面的代碼我們可以看出,條件語句如果用在while語句中是會有驚人的變化的,如果條件結果為true,那么意味着是死循環,此時編譯器會將while語句編程do while循環(死循環沒什么區別了),但是如果條件是false,那么編譯器會提示Unreachable Code,意思是下面代碼執行不到,此時會提示我們刪除下面執行不到的代碼作為解決辦法。看到這里我們已經很明白條件語句也是語法糖的一個成員了。
總結:
上面我們介紹了四種語法糖,實際上Java中除了這四種還有很多其他的語法糖,如變長參數、枚舉類、內部類,斷言語句等,這些我們以后遇到再一一介紹,這里不再贅述。對於語法糖我們只需要把握住一點,那就是“計算機語言中添加的某種語法,這種語法對語言的功能並沒有影響,但是更方便程序員使用。通常來說使用語法糖能夠增加程序的可讀性,從而減少程序代碼出錯的機會。”,有了這個認識,我們就能很容易分辨Java中的那些語法糖了.