語法糖

語法糖

  接觸語法糖是在讀《深刻理解Java虛擬機》的時候,初始以爲語法糖是個挺有意思的概念,遂抽出一週實踐詳細總結一下語法糖。百度百科對於語法糖的解釋以下;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>就是兩個不一樣的類型,它們在系統運行期生成,有本身的需方法表和類型數據,這種實現稱爲類型膨脹,基於這種方法實現的泛型稱爲真實泛型。說白了真實泛型不論是在源代碼仍是機器碼中都是可以起到約束做用的。spa

  Java語言中的泛型則與C#的大相徑庭,它只會存在於程序源碼中(即只能在.java文件中看到泛型的存在),而在編譯後的字節碼文件中,就已經看不到泛型的痕跡了,此時已經替換爲原來的原生類型(Raw Type,也稱爲裸類型),而且在相應的地方插入類強制類型轉換的代碼,所以,對於運行期的Java語言來講,ArrayList<int>ArrayList<String>就是同一個類,因此泛型技術其實是Java語言的一顆語法糖,Java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型稱爲僞泛型。下面咱們來看一段代碼,而且比對一下編譯後的代碼能夠看出泛型擦除的痕跡;code

源代碼
@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組織對虛擬機規範作出了相應的修改,引入了諸如SignatureLocalVariableTypeTable等新的屬性用於解決伴隨泛型而來的參數類型的識別問題,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.5Java的一大飛躍,以後就是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中的那些語法糖了.

相關文章
相關標籤/搜索