1. 程式人生 > >Java的語法糖

Java的語法糖

可能 所有 速度 方法重載 地方 jdk 解釋 依賴關系 als

1.前言

  本文記錄內容來自《深入理解Java虛擬機》的第十章早期(編譯期)優化其中一節內容,其他的內容個人覺得暫時不需要過多關註,比如語法、詞法分析,語義分析和字節碼生成的過程等。主要關註的就是Java的一些語法糖是如何實現的。

  語法糖不會提供實質性的功能改進,但是它們或能提高效率,或能提升語法的嚴謹性,或能減少編碼出錯的可能。大量使用語法糖可能會迷失其中,不得要領,下面就介紹一下Java的語法糖的實現。

2.泛型與類型擦除

  JDK5新增了一個特性,就是泛型。本質是參數化類型的應用,就是所操作的數據類型被指定為一個參數,這種參數可以應用在類、接口和方法的創建中。

  Java沒有泛型的時候,只能通過Object是所有類型的父類和類型強制轉換兩個特點的配合來實現類型泛化,比如HashMap的get方法,返回的就是Object,因為map中一切皆有可能。但是這樣的操作帶來了一些風險,如果強轉的類型錯誤,就會在運行期間拋出異常,我們需要在編碼期間就發現這個問題。

  Java的泛型是一種偽泛型,不是C#那種傳統意義上的泛型。在C#中,List<int>和List<String>是兩種類型,但是Java中還是List。因為Java的泛型只存在於代碼中,編譯後泛型就消失了,這個就是所說的類型擦除,取而代之的就是插入了強轉代碼。將一段含有泛型的Java程序編譯後,再反編譯回來,就會發現反編譯的代碼中泛型消失了,新增的就是強轉代碼。

  為什麽要使用類型擦除的方式實現泛型的原因不得而知,但是這個確實是個吐槽的地方。有人說性能上泛型會由於強制轉型操作和運行期缺少針對類型的優化導致速度比真正的泛型慢。這個評價角度不太對,因為泛型是用於提升語義準確性的。Java的泛型會導致一些奇特的問題。

  比如重載:public static void method(List<String> list)和public static void method(List<Integer> list)這兩個方法存在是不會編譯通過的,因為類型擦除後是特征簽名一模一樣的。看似重載不正確的原因找到了:重載要求方法名相同,參數不同,返回值可同可不同。這裏參數、方法名一致肯定失敗了。但實際上不是,因為如果改一下一個返回String類型,一個返回Integer類型,按照重載的定義,返回值可以不同,這兩個方法應該還是一樣的,編譯不同過才對,而實際上是可以通過的。這又是為什麽呢?在Java語言中方法重載實際是要求方法具有不同的特征簽名,相同的方法名。特征簽名是一個方法中各個參數在常量池中的字段符號引用的集合,返回值不在其中,這樣也就能理解重載的要求是方法名相同,參數不同的含義了。而修改了返回值為什麽通過了呢?原因在於這不是重載的範疇,在Class文件格式中,只要描述符不是完全一致的兩個方法也可以共存。方法的描述符和返回值是有關系的,所以這兩個方法的描述符因為返回值的區別是不同的,可以共存,但不是重載。

3.自動裝箱、拆箱與遍歷循環

  從技術角度來說,這些語法糖在實現和思想上都不如泛型,但是這是使用最多的語法糖。

    自動裝箱的操作就是:Integer.valueof(i),在編譯階段替換成了這個

    自動拆箱的操作就是:Integer.intValue()

    集合的foreach操作是:for(its = list.iterator; its.hasNext; ;) { its.next},采取的是叠代器的方式遍歷

    數組的foreach操作實際上是壓棧,進棧出棧操作。

  這些原理都什麽簡單,可以自己寫一個類編譯後,再反編譯看看結果。

  自動裝箱的陷阱:

    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        System.out.println(c == d);
        System.out.println(e == f);
        System.out.println(c == (a+b));
        System.out.println(c.equals(a+b));
        System.out.println(g == (a+b));
        System.out.println(g.equals(a+b));
    }

  上面的代碼執行出來1.true 2.false 3.true 4.true 5.true 6.false。

  解釋一下上面的含義:

    首先包裝類的==比較不會觸發自動拆箱,所以1、2的比較是引用比較。那為什麽1是true,2是false?上面說了自動裝箱是通過Integer.valueof實現的,Java代碼對Integer進行了優化,查看源代碼可以看到有個緩存,在-128~127之間的整數,獲取的是同一個Integer對象,321超過了這個範圍,生成的是不同的對象。

    +號會觸發自動拆箱,導致==一般是int類型,而不是Integer類型,最後觸發c的自動拆箱,兩個int類型比較,值相等就是true了。

    4中的+號也會觸發自動拆箱,成int類型,但由於是equals方法,又進行了自動裝箱,最後判斷標準就是Integer.equals的方法,其比較就是兩個int類型進行比較,所以是true

    5中和3的步驟類似,但是g自動才行成了long類型,long與int比較,數值的大小比較。

    6和4類似,但是最終變成了Long.equals(Integer),根據Long的equals方法,傳入的必須是Long類型才可,所以返回的是false。

  這個例子告訴我們代碼中避免這樣使用自動裝箱和拆箱,掌握不牢可能發生意料之外的情況。

4.條件編譯

  許多語言都提供了條件編譯的途徑,比如C、C++的預處理器指示符#ifdef來完成條件編譯,他們是用於解決編譯時代碼的依賴關系,但在Java中沒有使用預處理器,因為不需要,編譯器不是一個個編譯Java文件,而是將所有編譯單元的語法樹頂級節點輸入到待處理列表後再進行編譯。

  Java語言也可以實現條件編譯,方法就是使用條件是常量的if語句,比如if(true){} else{},這樣else模塊的內容就會被省略。

  這種條件編譯只能寫在方法體內,沒有辦法根據條件調整整個Java類結構。

5.其他語法糖

  內部類、枚舉類、斷言語句、對枚舉和字符串(JDK7)的switch支持、try中定義和關閉資源等。這些可以自己通過javac編譯後javap -verbose進行查看字節碼,理解其實現原理。

Java的語法糖