Java 中的語法糖,真甜。

我把本身以往的文章彙總成爲了 Github ,歡迎各位大佬 star
https://github.com/crisxuan/bestJavaerjava

咱們在平常開發中常常會使用到諸如泛型、自動拆箱和裝箱、內部類、加強 for 循環、try-with-resources 語法、lambda 表達式等,咱們只以爲用的很爽,由於這些特性可以幫助咱們減輕開發工做量;但咱們不曾認真研究過這些特性的本質是什麼,那麼這篇文章,cxuan 就來爲你揭開這些特性背後的真相。c++

語法糖

在聊以前咱們須要先了解一下 語法糖 的概念:語法糖(Syntactic sugar),也叫作糖衣語法,是英國科學家發明的一個術語,一般來講使用語法糖可以增長程序的可讀性,從而減小程序代碼出錯的機會,真是又香又甜。git

語法糖指的是計算機語言中添加的某種語法,這種語法對語言的功能並無影響,可是更方便程序員使用。由於 Java 代碼須要運行在 JVM 中,JVM 是並不支持語法糖的,語法糖在程序編譯階段就會被還原成簡單的基礎語法結構,這個過程就是解語法糖。因此在 Java 中,真正支持語法糖的是 Java 編譯器,真是換湯不換藥,萬變不離其宗,關了燈都同樣。。。。。。程序員

下面咱們就來認識一下 Java 中的這些語法糖github

泛型

泛型是一種語法糖。在 JDK1.5 中,引入了泛型機制,可是泛型機制的自己是經過類型擦除 來實現的,在 JVM 中沒有泛型,只有普通類型和普通方法,泛型類的類型參數,在編譯時都會被擦除。泛型並無本身獨特的 Class類型。以下代碼所示數組

List<Integer> aList = new ArrayList();
List<String> bList = new ArrayList();

System.out.println(aList.getClass() == bList.getClass());

List<Ineger>List<String> 被認爲是不一樣的類型,可是輸出卻獲得了相同的結果,這是由於,泛型信息只存在於代碼編譯階段,在進入 JVM 以前,與泛型相關的信息會被擦除掉,專業術語叫作類型擦除。可是,若是將一個 Integer 類型的數據放入到 List<String> 中或者將一個 String 類型的數據放在 List<Ineger> 中是不容許的。微信

以下圖所示app

沒法將一個 Integer 類型的數據放在 List<String> 和沒法將一個 String 類型的數據放在 List<Integer> 中是同樣會編譯失敗。框架

自動拆箱和自動裝箱

自動拆箱和自動裝箱是一種語法糖,它說的是八種基本數據類型的包裝類和其基本數據類型之間的自動轉換。簡單的說,裝箱就是自動將基本數據類型轉換爲包裝器類型;拆箱就是自動將包裝器類型轉換爲基本數據類型。性能

咱們先來了解一下基本數據類型的包裝類都有哪些

也就是說,上面這些基本數據類型和包裝類在進行轉換的過程當中會發生自動裝箱/拆箱,例以下面代碼

Integer integer = 66; // 自動拆箱

int i1 = integer;   // 自動裝箱

上面代碼中的 integer 對象會使用基本數據類型來進行賦值,而基本數據類型 i1 卻把它賦值給了一個對象類型,通常狀況下是不能這樣操做的,可是編譯器卻容許咱們這麼作,這其實就是一種語法糖。這種語法糖使咱們方便咱們進行數值運算,若是沒有語法糖,在進行數值運算時,你須要先將對象轉換成基本數據類型,基本數據類型同時也須要轉換成包裝類型才能使用其內置的方法,無疑增長了代碼冗餘。

那麼自動拆箱和自動裝箱是如何實現的呢?

其實這背後的原理是編譯器作了優化。將基本類型賦值給包裝類實際上是調用了包裝類的 valueOf() 方法建立了一個包裝類再賦值給了基本類型。

int i1 = Integer.valueOf(1);

而包裝類賦值給基本類型就是調用了包裝類的 xxxValue() 方法拿到基本數據類型後再進行賦值。

Integer i1 = new Integer(1).intValue();

咱們使用 javap -c 反編譯一下上面的自動裝箱和自動拆箱來驗證一下

能夠看到,在 Code 2 處調用 invokestatic 的時候,至關因而編譯器自動爲咱們添加了一下 Integer.valueOf 方法從而把基本數據類型轉換爲了包裝類型。

在 Code 7 處調用了 invokevirtual 的時候,至關因而編譯器爲咱們添加了 Integer.intValue() 方法把 Integer 的值轉換爲了基本數據類型。

枚舉

咱們在平常開發中常常會使用到 enumpublic static final ... 這類語法。那麼何時用 enum 或者是 public static final 這類常量呢?好像均可以。

可是在 Java 字節碼結構中,並無枚舉類型。枚舉只是一個語法糖,在編譯完成後就會被編譯成一個普通的類,也是用 Class 修飾。這個類繼承於 java.lang.Enum,並被 final 關鍵字修飾

咱們舉個例子來看一下

public enum School {
    STUDENT,
    TEACHER;
}

這是一個 School 的枚舉,裏面包括兩個字段,一個是 STUDENT ,一個是 TEACHER,除此以外並沒有其餘。

下面咱們使用 javap 反編譯一下這個 School.class 。反編譯完成以後的結果以下

從圖中咱們能夠看到,枚舉其實就是一個繼承於 java.lang.Enum 類的 class 。而裏面的屬性 STUDENT 和 TEACHER 本質也就是 public static final 修飾的字段。這其實也是一種編譯器的優化,畢竟 STUDENT 要比 public static final School STUDENT 的美觀性、簡潔性都要好不少。

除此以外,編譯器還會爲咱們生成兩個方法,values() 方法和 valueOf 方法,這兩個方法都是編譯器爲咱們添加的方法,經過使用 values() 方法能夠獲取全部的 Enum 屬性值,而經過 valueOf 方法用於獲取單個的屬性值。

注意,Enum 的 values() 方法不屬於 JDK API 的一部分,在 Java 源碼中,沒有 values() 方法的相關注釋。

用法以下

public enum School {

    STUDENT("Student"),
    TEACHER("Teacher");

    private String name;

    School(String name){
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public static void main(String[] args) {

        System.out.println(School.STUDENT.getName());

        School[] values = School.values();
        for(School school : values){
            System.out.println("name = "+ school.getName());
        }

    }
}

內部類

內部類是 Java 一個小衆 的特性,我之因此說小衆,並非說內部類沒有用,而是咱們平常開發中其實不多用到,可是翻看 JDK 源碼,發現不少源碼中都有內部類的構造。好比常見的 ArrayList 源碼中就有一個 Itr 內部類繼承於 Iterator 類;再好比 HashMap 中就構造了一個 Node 繼承於 Map.Entry<K,V> 來表示 HashMap 的每個節點。

Java 語言中之因此引入內部類,是由於有些時候一個類只想在一個類中有用,不想讓其在其餘地方被使用,也就是對外隱藏內部細節。

內部類其實也是一個語法糖,由於其只是一個編譯時的概念,一旦編譯完成,編譯器就會爲內部類生成一個單獨的class 文件,名爲 outer$innter.class。

下面咱們就根據一個示例來驗證一下。

public class OuterClass {

    private String label;

    class InnerClass {

        public String linkOuter(){
            return label = "inner";
        }

    }
    public static void main(String[] args) {

        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        System.out.println(innerClass.linkOuter());

    }
}

上面這段編譯後就會生成兩個 class 文件,一個是 OuterClass.class ,一個是 OuterClass$InnerClass.class ,這就代表,外部類能夠連接到內部類,內部類能夠修改外部類的屬性等。

咱們來看一下內部類編譯後的結果

如上圖所示,內部類通過編譯後的 linkOuter() 方法會生成一個指向外部類的 this 引用,這個引用就是鏈接外部類和內部類的引用。

變長參數

變長參數也是一個比較小衆的用法,所謂變長參數,就是方法能夠接受長度不定肯定的參數。通常咱們開發不會使用到變長參數,並且變長參數也不推薦使用,它會使咱們的程序變的難以處理。可是咱們有必要了解一下變長參數的特性。

其基本用法以下

public class VariableArgs {

    public static void printMessage(String... args){
        for(String str : args){
            System.out.println("str = " + str);
        }
    }

    public static void main(String[] args) {
        VariableArgs.printMessage("l","am","cxuan");
    }
}

變長參數也是一種語法糖,那麼它是如何實現的呢?咱們能夠猜想一下其內部應該是由數組構成,不然沒法接受多個值,那麼咱們反編譯看一下是否是由數組實現的。

能夠看到,printMessage() 的參數就是使用了一個數組來接收,因此千萬別被變長參數忽悠了!

變長參數特性是在 JDK 1.5 中引入的,使用變長參數有兩個條件,一是變長的那一部分參數具備相同的類型,二是變長參數必須位於方法參數列表的最後面。

加強 for 循環

爲何有了普通的 for 循環後,還要有加強 for 循環呢?想一下,普通 for 循環你不是須要知道遍歷次數?每次還須要知道數組的索引是多少,這種寫法明顯有些繁瑣。加強 for 循環與普通 for 循環相比,功能更強而且代碼更加簡潔,你無需知道遍歷的次數和數組的索引便可進行遍歷。

加強 for 循環的對象要麼是一個數組,要麼實現了 Iterable 接口。這個語法糖主要用來對數組或者集合進行遍歷,其在循環過程當中不能改變集合的大小。

public static void main(String[] args) {
    String[] params = new String[]{"hello","world"};
    //加強for循環對象爲數組
    for(String str : params){
        System.out.println(str);
    }

    List<String> lists = Arrays.asList("hello","world");
    //加強for循環對象實現Iterable接口
    for(String str : lists){
        System.out.println(str);
    }
}

通過編譯後的 class 文件以下

public static void main(String[] args) {
   String[] params = new String[]{"hello", "world"};
   String[] lists = params;
   int var3 = params.length;
   //數組形式的加強for退化爲普通for
   for(int str = 0; str < var3; ++str) {
       String str1 = lists[str];
       System.out.println(str1);
   }

   List var6 = Arrays.asList(new String[]{"hello", "world"});
   Iterator var7 = var6.iterator();
   //實現Iterable接口的加強for使用iterator接口進行遍歷
   while(var7.hasNext()) {
       String var8 = (String)var7.next();
       System.out.println(var8);
   }

}

如上代碼所示,若是對數組進行加強 for 循環的話,其內部仍是對數組進行遍歷,只不過語法糖把你忽悠了,讓你以一種更簡潔的方式編寫代碼。

而對繼承於 Iterator 迭代器進行加強 for 循環遍歷的話,至關因而調用了 Iterator 的 hasNext()next() 方法。

Switch 支持字符串和枚舉

switch 關鍵字原生只能支持整數類型。若是 switch 後面是 String 類型的話,編譯器會將其轉換成 String 的hashCode 的值,因此其實 switch 語法比較的是 String 的 hashCode 。

以下代碼所示

public class SwitchCaseTest {

    public static void main(String[] args) {

        String str = "cxuan";
        switch (str){
            case "cuan":
                System.out.println("cuan");
                break;
            case "xuan":
                System.out.println("xuan");
                break;
            case "cxuan":
                System.out.println("cxuan");
                break;
            default:
                break;
        }
    }
}

咱們反編譯一下,看看咱們的猜測是否正確

根據字節碼能夠看到,進行 switch 的實際是 hashcode 進行判斷,而後經過使用 equals 方法進行比較,由於字符串有可能會產生哈希衝突的現象。

條件編譯

這個又是讓小夥伴們摸不着頭腦了,什麼是條件編譯呢?其實,若是你用過 C 或者 C++ 你就知道能夠經過預處理語句來實現條件編譯。

那麼什麼是條件編譯呢?

通常狀況下,源程序中全部的行都參加編譯。但有時但願對其中一部份內容只在知足必定條件下才進行編譯,即對一部份內容指定編譯條件,這就是 條件編譯(conditional compile)

#define DEBUG  
#IFDEF DEBUUG  
  /* 
   code block 1 
   */   
#ELSE  
  /* 
   code block 2 
  */  
#ENDIF

可是在 Java 中沒有預處理和宏定義這些內容,那麼咱們想實現條件編譯,應該怎樣作呢?

使用 final + if 的組合就能夠實現條件編譯了。以下代碼所示

public static void main(String[] args) {  
  final boolean DEBUG = true;  
  if (DEBUG) {  
    System.out.println("Hello, world!");  
  }  else {
    System.out.println("nothing");
  }
}

這段代碼會發生什麼?咱們反編譯看一下

咱們能夠看到,咱們明明是使用了 if ...else 語句,可是編譯器卻只爲咱們編譯了 DEBUG = true 的條件,

因此,Java 語法的條件編譯,是經過判斷條件爲常量的 if 語句實現的,編譯器不會爲咱們編譯分支爲 false 的代碼。

斷言

你在 Java 中使用過斷言做爲平常的判斷條件嗎?

斷言:也就是所謂的 assert 關鍵字,是 jdk 1.4 後加入的新功能。它主要使用在代碼開發和測試時期,用於對某些關鍵數據的判斷,若是這個關鍵數據不是你程序所預期的數據,程序就提出警告或退出。當軟件正式發佈後,能夠取消斷言部分的代碼。它也是一個語法糖嗎?如今我不告訴你,咱們先來看一下 assert 如何使用。

//這個成員變量的值能夠變,但最終必須仍是回到原值5  
static int i = 5;  
public static void main(String[] args) {  
  assert i == 5;  
  System.out.println("若是斷言正常,我就被打印");  
}

若是要開啓斷言檢查,則須要用開關 -enableassertions 或 -ea 來開啓。其實斷言的底層實現就是 if 判斷,若是斷言結果爲 true,則什麼都不作,程序繼續執行,若是斷言結果爲 false,則程序拋出 AssertError 來打斷程序的執行。

assert 斷言就是經過對布爾標誌位進行了一個 if 判斷。

try-with-resources

JDK 1.7 開始,java引入了 try-with-resources 聲明,將 try-catch-finally 簡化爲 try-catch,這實際上是一種語法糖,在編譯時會進行轉化爲 try-catch-finally 語句。新的聲明包含三部分:try-with-resources 聲明、try 塊、catch 塊。它要求在 try-with-resources 聲明中定義的變量實現了 AutoCloseable 接口,這樣在系統能夠自動調用它們的 close 方法,從而替代了 finally 中關閉資源的功能。

以下代碼所示

public class TryWithResourcesTest {

    public static void main(String[] args) {
        try(InputStream inputStream = new FileInputStream(new File("xxx"))) {
            inputStream.read();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

咱們能夠看一下 try-with-resources 反編譯以後的代碼

能夠看到,生成的 try-with-resources 通過編譯後仍是使用的 try...catch...finally 語句,只不過這部分工做由編譯器替咱們作了,這樣能讓咱們的代碼更加簡潔,從而消除樣板代碼。

字符串相加

這個想必你們應該都知道,字符串的拼接有兩種,若是可以在編譯時期肯定拼接的結果,那麼使用 + 號鏈接的字符串會被編譯器直接優化爲相加的結果,若是編譯期不能肯定拼接的結果,底層會直接使用 StringBuilderappend 進行拼接,以下圖所示。

public class StringAppendTest {

    public static void main(String[] args) {
        String s1 = "I am " + "cxuan";
        String s2 = "I am " + new String("cxuan");
        String s3 = "I am ";
        String s4 = "cxuan";
        String s5 = s3 + s4;

    }
}

上面這段代碼就包含了兩種字符串拼接的結果,咱們反編譯看一下

首先來看一下 s1 ,s1 由於 = 號右邊是兩個常量,因此兩個字符串拼接會被直接優化成爲 I am cxuan。而 s2 因爲在堆空間中分配了一個 cxuan 對象,因此 + 號兩邊進行字符串拼接會直接轉換爲 StringBuilder ,調用其 append 方法進行拼接,最後再調用 toString() 方法轉換成字符串。

而因爲 s5 進行拼接的兩個對象在編譯期不能斷定其拼接結果,因此會直接使用 StringBuilder 進行拼接。

學習語法糖的意義

互聯網時代,有不少標新立異的想法和框架層出不窮,可是,咱們對於學習來講應該抓住技術的核心。然而,軟件工程是一門協做的藝術,對於工程來講如何提升工程質量,如何提升工程效率也是咱們要關注的,既然這些語法糖能輔助咱們以更好的方式編寫備受歡迎的代碼,咱們程序員爲何要 抵制 呢?

語法糖也是一種進步,這就和你寫做文似的,大白話能把故事講明白的它就沒有語言優美、酣暢淋漓的把故事講生動的更使人喜歡。

咱們要在敞開懷抱擁抱變化的同時也要掌握其 屠龍之技

另外,我本身肝了六本 PDF,微信搜索「程序員cxuan」關注公衆號後,在後臺回覆 cxuan ,領取所有 PDF,這些 PDF 以下

六本 PDF 連接

相關文章
相關標籤/搜索