1. 程式人生 > >Java 語法糖詳解

Java 語法糖詳解

語法糖

語法糖(Syntactic Sugar),也稱糖衣語法,是由英國計算機學家 Peter.J.Landin 發明的一個術語,指在計算機語言中新增的某種語法。

這種語法對語言的功能並沒有影響,但是更方便程式設計師使用。簡而言之,語法糖讓程式更加簡潔,有更高的可讀性。

有意思的是,在程式設計領域,除了語法糖,還有語法鹽和語法糖精的說法,篇幅有限這裡不做擴充套件了。

我們所熟知的程式語言中幾乎都有語法糖。作者認為,語法糖的多少是評判一個語言夠不夠牛逼的標準之一。

很多人說 Java 是一個 “低糖語言”,其實從 Java 7 開始 Java 語言層面上一直在新增各種糖,主要是在 “Project Coin” 專案下研發。儘管現在 Java 有人還是認為現在的 Java 是低糖,未來還會持續向著 “高糖” 的方向發展。

解語法糖
前面提到過,語法糖的存在主要是方便開發人員使用。但其實,Java 虛擬機器並不支援這些語法糖,這些語法糖在編譯階段就會被還原成簡單的基礎語法結構,這個過程就是解語法糖。

說到編譯,大家肯定都知道,Java 語言中,javac命令可以將字尾名為.java的原始檔編譯為字尾名為.class的可以運行於 Java 虛擬機器的位元組碼。

如果你去看com.sun.tools.javac.main.JavaCompiler的原始碼,你會發現在compile()中有一個步驟就是呼叫desugar(),這個方法就是負責解語法糖的實現的。

Java 中最常用的語法糖主要有泛型、變長引數、條件編譯、自動拆裝箱、內部類等。本文主要來分析下這些語法糖背後的原理,一步一步剝去糖衣,看看其本質。

糖塊一、switch 支援 String 與列舉
前面提到過,從 Java 7 開始,Java 語言中的語法糖在逐漸豐富,其中一個比較重要的就是 Java 7 中switch開始支援String。

在開始 coding 之前先科普下,Java 中的swith自身原本就支援基本型別。比如int、char等。對於int型別,直接進行數值的比較。

對於char型別則是比較其 ascii 碼。所以,對於編譯器來說,switch中其實只能使用整型,任何型別的比較都要轉換成整型。

比如byte、short、char(ackii 碼是整型)以及int。

那麼接下來看下switch對String得支援,有以下程式碼:

   public class switchDemoString {        public static void main(String[] args) {            String str = "world";            switch (str) {            case "hello":                System.out.println("hello");                break;            case "world":                System.out.println("world");                break;            default:                break;            }        }    }

反編譯(http://www.hollischuang.com/archives/58) 後內容如下:

   public class switchDemoString    {        public switchDemoString()        {        }        public static void main(String args[])        {            String str = "world";            String s;            switch((s = str).hashCode())            {            default:                break;            case 99162322:                if(s.equals("hello"))                    System.out.println("hello");                break;            case 113318802:                if(s.equals("world"))                    System.out.println("world");                break;            }        }    }

看到這個程式碼,你知道原來字串的 switch 是通過equals()和hashCode()方法來實現的。還好hashCode()方法返回的是int,而不是long。

仔細看下可以發現,進行switch的實際是雜湊值,然後通過使用equals方法比較進行安全檢查,這個檢查是必要的,因為雜湊可能會發生碰撞。因此它的效能是不如使用列舉進行 switch 或者使用純整數常量,但這也不是很差。

糖塊二、泛型
我們都知道,很多語言都是支援泛型的,但是很多人不知道的是,不同的編譯器對於泛型的處理方式是不同的。

通常情況下,一個編譯器處理泛型有兩種方式:Code specialization和Code sharing。C++ 和 C#是使用Code specialization的處理機制,而 Java 使用的是Code sharing的機制。

Code sharing 方式為每個泛型型別建立唯一的位元組碼錶示,並且將該泛型型別的例項都對映到這個唯一的位元組碼錶示上。將多種泛型類形例項對映到唯一的位元組碼錶示是通過型別擦除(type erasue)實現的。

也就是說,對於 Java 虛擬機器來說,他根本不認識Map<String, String> map這樣的語法。需要在編譯階段通過型別擦除的方式進行解語法糖。

型別擦除的主要過程如下:

將所有的泛型引數用其最左邊界(最頂級的父型別)型別替換。

移除所有的型別引數。

以下程式碼:

   Map<String, String> map = new HashMap<String, String>();      map.put("name", "hollis");      map.put("wechat", "Hollis");      map.put("blog", "www.hollischuang.com");

解語法糖之後會變成:

   Map map = new HashMap();      map.put("name", "hollis");      map.put("wechat", "Hollis");      map.put("blog", "www.hollischuang.com");

以下程式碼:

   public static <A extends Comparable<A>> A max(Collection<A> xs) {        Iterator<A> xi = xs.iterator();        A w = xi.next();        while (xi.hasNext()) {            A x = xi.next();            if (w.compareTo(x) < 0)                w = x;        }        return w;    }

型別擦除後會變成:

    public static Comparable max(Collection xs){        Iterator xi = xs.iterator();        Comparable w = (Comparable)xi.next();        while(xi.hasNext())        {            Comparable x = (Comparable)xi.next();            if(w.compareTo(x) < 0)                w = x;        }        return w;    }

虛擬機器中沒有泛型,只有普通類和普通方法,所有泛型類的型別引數在編譯時都會被擦除,泛型類並沒有自己獨有的Class類物件。

比如並不存在List<String>.class或是List<Integer>.class,而只有List.class。

糖塊三、自動裝箱與拆箱
自動裝箱就是 Java 自動將原始型別值轉換成對應的物件,比如將 int 的變數轉換成 Integer 物件,這個過程叫做裝箱。

反之將 Integer 物件轉換成 int 型別值,這個過程叫做拆箱。因為這裡的裝箱和拆箱是自動進行的非人為轉換,所以就稱作為自動裝箱和拆箱。

原始型別 byte、short、char、int、long、float、double 和 boolean 對應的封裝類為 Byte、Short、Character、Integer、Long、Float、Double、Boolean。

先來看個自動裝箱的程式碼:

    public static void main(String[] args) {        int i = 10;        Integer n = i;    }

反編譯後代碼如下:

   public static void main(String args[])    {        int i = 10;        Integer n = Integer.valueOf(i);    }

再來看個自動拆箱的程式碼:

   public static void main(String[] args) {        Integer i = 10;        int n = i;    }

反編譯後代碼如下:

   public static void main(String args[])    {        Integer i = Integer.valueOf(10);        int n = i.intValue();    }

從反編譯得到內容可以看出,在裝箱的時候自動呼叫的是Integer的valueOf(int)方法。而在拆箱的時候自動呼叫的是Integer的intValue方法。

所以,裝箱過程是通過呼叫包裝器的 valueOf 方法實現的,而拆箱過程是通過呼叫包裝器的 xxxValue 方法實現的。

糖塊四、方法變長引數
可變引數 (variable arguments) 是在 Java 1.5 中引入的一個特性,它允許一個方法把任意數量的值作為引數。

看下以下可變引數程式碼,其中 print 方法接收可變引數:

   public static void main(String[] args)        {            print("Holis", " 公眾號:Hollis", " 部落格:www.hollischuang.com", "QQ:907607222");        }    public static void print(String... strs)    {        for (int i = 0; i < strs.length; i++)        {            System.out.println(strs[i]);        }    }

反編譯後代碼:

    public static void main(String args[])    {        print(new String[] {            "Holis", "\u516C\u4F17\u53F7:Hollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com", "QQ\uFF1A907607222"        });    }    public static transient void print(String strs[])    {        for(int i = 0; i < strs.length; i++)            System.out.println(strs[i]);    }

從反編譯後代碼可以看出,可變引數在被使用的時候,他首先會建立一個數組,陣列的長度就是呼叫該方法是傳遞的實參的個數。

然後再把引數值全部放到這個陣列當中,然後再把這個陣列作為引數傳遞到被呼叫的方法中。

反編譯後的 print 方法宣告中有一個 transient 標識,是不是很奇怪?transient 不是不可以修飾方法嗎?transient 不是和序列化有關麼?transient 在這裡的作用是什麼?因為這個與本文關係不大,這裡不做深入分析了。

糖塊五、列舉
Java SE5 提供了一種新的型別-Java 的列舉型別,關鍵字enum可以將一組具名的值的有限集合建立為一種新的型別,而這些具名的值可以作為常規的程式元件使用,這是一種非常有用的功能。

要想看原始碼,首先得有一個類吧,那麼列舉型別到底是什麼類呢?是enum嗎?答案很明顯不是,enum就和class一樣,只是一個關鍵字,他並不是一個類,那麼列舉是由什麼類維護的呢,我們簡單寫一個列舉:

   public enum t {        SPRING,SUMMER;    }

然後我們使用反編譯,看看這段程式碼到底是怎麼實現的,反編譯後代碼內容如下:

   public final class T extends Enum    {        private T(String s, int i)        {            super(s, i);        }        public static T[] values()        {            T at[];            int i;            T at1[];            System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);            return at1;        }        public static T valueOf(String s)        {            return (T)Enum.valueOf(demo/T, s);        }        public static final T SPRING;        public static final T SUMMER;        private static final T ENUM$VALUES[];        static        {            SPRING = new T("SPRING", 0);            SUMMER = new T("SUMMER", 1);            ENUM$VALUES = (new T[] {                SPRING, SUMMER            });        }    }

通過反編譯後代碼我們可以看到,public final class T extends Enum,說明,該類是繼承了Enum類的,同時final關鍵字告訴我們,這個類也是不能被繼承的。

當我們使用enmu來定義一個列舉型別的時候,編譯器會自動幫我們建立一個final型別的類繼承Enum類,所以列舉型別不能被繼承。

糖塊六、內部類
內部類又稱為巢狀類,可以把內部類理解為外部類的一個普通成員。

內部類之所以也是語法糖,是因為它僅僅是一個編譯時的概念。

outer.java裡面定義了一個內部類inner,一旦編譯成功,就會生成兩個完全不同的.class檔案了,分別是outer.class和outer$inner.class。所以內部類的名字完全可以和它的外部類名字相同。

   public class OutterClass {        private String userName;        public String getUserName() {            return userName;        }        public void setUserName(String userName) {            this.userName = userName;        }        public static void main(String[] args) {        }        class InnerClass{            private String name;            public String getName() {                return name;            }            public void setName(String name) {                this.name = name;            }        }    }

以上程式碼編譯後會生成兩個 class 檔案:OutterClass$InnerClass.class 、OutterClass.class 。

當我們嘗試對OutterClass.class檔案進行反編譯的時候,命令列會列印以下內容:Parsing OutterClass.class...Parsing inner class OutterClass$InnerClass.class... Generating OutterClass.jad 。

它會把兩個檔案全部進行反編譯,然後一起生成一個OutterClass.jad檔案。檔案內容如下:

   public class OutterClass    {        class InnerClass        {            public String getName()            {                return name;            }            public void setName(String name)            {                this.name = name;            }            private String name;            final OutterClass this$0;            InnerClass()            {                this.this$0 = OutterClass.this;                super();            }        }        public OutterClass()        {        }        public String getUserName()        {            return userName;        }        public void setUserName(String userName){            this.userName = userName;        }        public static void main(String args1[])        {        }        private String userName;    }
---------------------
作者:GitChat技術雜談
來源:CSDN
原文:https://blog.csdn.net/GitChat/article/details/79294904
版權宣告:本文為博主原創文章,轉載請附上博文連結!