淺析java中的語法糖

概述

編譯器是一種計算機程序, 它主要的目的是將便於人編寫、閱讀、維護的高級計算機語言所寫的源代碼程序, 翻譯爲計算機能解讀、運行的低階機器語言的程序, 便可執行文件。而 javac 就是java語言中的編譯器, 它用於將 .java 文件轉換成JVM能識別的 .class 字節碼文件, 反編譯則是將 .class 文件轉換成 .java 文件。html

語法糖(Syntactic sugar),也譯爲糖衣語法,是由英國計算機科學家彼得·蘭丁發明的一個術語,指計算機語言中添加的某種語法,這種語法對語言的功能沒有影響,可是更方便程序員使用。語法糖讓程序更加簡潔,有更高的可讀性。java

java中的語法糖只存在於編譯期, 在編譯器將 .java 源文件編譯成 .class 字節碼時, 會進行解語法糖操做, 還原最原始的基礎語法結構。這些語法糖包含條件編譯、斷言、Switch語句與枚舉及字符串結合、可變參數、自動裝箱/拆箱、枚舉、內部類、泛型擦除、加強for循環、lambda表達式、try-with-resources語句、JDK10的局部變量類型推斷等等。程序員

關於反編譯工具, 其實在JDK中自帶了一個javap命令, 在之前的文章JDK的命令行工具系列 (二) javap、jinfo、jmap中也有說起到, 可是平常中不多會用到javap, 因此此次咱們藉助另外一個反編譯工具 CFR 來分析java中的語法糖, 這裏我下載的是最新的cfr_0_132.jar數據庫

字符串拼接

/**
 * 字符串拼接
 * option: --stringbuilder false
 */
public void stringBuilderTest(int end) {
    char[] foo = new char[]{'@', 'a', '*'};
    char ch;
    int x = 0;
    while ((ch = foo[++x]) != '*') {
        System.out.println("" + x + ": " + ch);
    }
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --stringbuilder false數組

從反編譯後的代碼中能看出, 當咱們使用+號進行字符串拼接操做時, 編譯時會自動建立一個StringBuilder對象。因此當在循環中拼接字符串時, 應避免使用+號操做, 不然每次循環都會建立一個StringBuilder對象再回收, 形成較大的開銷。安全

條件編譯

/**
 * 條件編譯
 * option: 不須要參數
 */
public void ifCompilerTest() {
    if(false) {
        System.out.println("false if");
    }else {
        System.out.println("true else");
    }
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.classide

很明顯, javac編譯器在編譯時期的解語法糖階段, 會將條件分支不成立的代碼進行消除。函數

斷言

/**
 * 斷言, JDK1.4開始支持
 * option: --sugarasserts false
 */
public void assertTest(String s) {
    assert (!s.equals("Fred"));
    System.out.println(s);
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --sugarasserts false工具

如上, 當斷言結果爲true時, 程序繼續正常執行, 當斷言結果爲false時, 則拋出AssertionError異常來打斷程序的執行。oop

枚舉與Switch語句

/**
 * 枚舉與Switch語句
 * option: --decodeenumswitch false
 */
public int switchEnumTest(EnumTest e) {
    switch (e) {
        case FOO:
            return 1;
        case BAP:
            return 2;
    }
    return 0;
}

/**
 * 枚舉, JDK1.5開始支持
 * option: --sugarenums false
 */
public enum EnumTest {
    FOO,
    BAR,
    BAP
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --decodeenumswitch false

switch支持枚舉是經過調用枚舉類默認繼承的父類Enum中的ordinal()方法來實現的, 這個方法會返回枚舉常量的序數。因爲筆者的經驗尚淺, 具體的實現細節還不是很清楚(好比枚舉常量FOO的序數是0, 而case FOO語句編譯後的 case 1, 這個1是什麼? 另外switchEnumTest()方法傳入一個FOO, 調用ordinal()方法獲得的序數爲0, 那麼他又是如何與case 1進行匹配的呢?), 歡迎讀者在留言區一塊兒討論。

字符串與Switch語句

/** 
 * 字符串與Switch語句
 * option: --decodestringswitch false
 */
public int switchStringTest(String s) {
    switch (s) {
        default:
            System.out.println("Test");
            break;
        case "BB":  // BB and Aa have the same hashcode.
            return 12;
        case "Aa":
        case "FRED":
            return 13;
    }
    System.out.println("Here");
    return 0;
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --decodestringswitch false

switch支持字符串是經過hashCode()equals()方法來實現的, 先經過hashCode()返回的哈希值進行switch, 而後經過equals()方法比較進行安全檢查, 調用equals()是爲了防止可能發生的哈希碰撞。

另外switch還支持byteshortintchar這幾種基本數據類型, 其中支持char類型是經過比較它們的ascii碼(ascii碼是整型)來實現的。因此switch其實只支持一種數據類型, 也就是整型, 其餘諸如String、枚舉類型都是轉換成整型以後再使用switch的。

可變參數

/**
 * 可變參數
 * option: --arrayiter false
 */
public void varargsTest(String ... arr) {
    for (String s : arr) {
        System.out.println(s);
    }
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --arrayiter false

可變參數其實就是一個不定長度的數組, 數組長度隨傳入方法的對應參數個數來決定。可變參數只能在參數列表的末位使用。

自動裝箱/拆箱

/**
 * 自動裝箱/拆箱
 * option: --sugarboxing false
 */
public Double autoBoxingTest(Integer i, Double d) {
    return d + i;
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --sugarboxing false

首先咱們知道, 基本類型與包裝類型在某些操做符的做用下, 包裝類型調用valueOf()方法的過程叫作裝箱, 調用xxxValue()方法的過程叫作拆箱。因此上面的結果很容易看出, 先對兩個包裝類進行拆箱, 再對運算結果進行裝箱。

枚舉

/**
 * 枚舉, JDK1.5開始支持
 * option: --sugarenums false
 */
public enum EnumTest {
    FOO,
    BAR,
    BAP
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --sugarenums false

當咱們自定義一個枚舉類型時, 編譯器會自動建立一個被final修飾的枚舉類來繼承Enum, 因此自定義枚舉類型是沒法繼承和被繼承的。當枚舉類初始化時, 枚舉字段引用該枚舉類的一個靜態常量對象, 而且全部的枚舉字段都用常量數組$VALUES來存儲。values()方法內則調用Object的clone()方法, 參照$VALUES數組對象複製一個新的數組, 新數組會有全部的枚舉字段。

內部類

import java.util.*;
import java.io.*;

public class CFRDecompilerDemo {

    int x = 3;

    /**
     * 內部類
     * option: --removeinnerclasssynthetics false
     */
    public void innerClassTest() {
        new InnerClass().getSum(6);
    }

    public class InnerClass {
        public int getSum(int y) {
            x += y;
            return x;
        }
    }    
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --removeinnerclasssynthetics false

首先咱們要明確, 上述innerClassTest()方法中的this是外部類當前對象的引用, 而InnerClass類中的this則是內部類當前對象的引用。編譯過程當中, 編譯器會自動在內部類定義一個外部類的常量引用this$0, 而且在內部類的構造器中初始化this$0, 當外部類訪問內部類時, 會把當前外部類的對象引用this傳給內部類的構造器用於初始化, 這樣內部類就能經過所持有的外部類的對象引用, 來訪問外部類的全部公有及私有成員。

泛型擦除

/**
 * 泛型擦除
 * option: 
 */
public void genericEraseTest() {
    List<String> list =  new ArrayList<String>();
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class

在JVM中沒有泛型這一律念,  只有普通方法和普通類, 全部泛型類的泛型參數都會在編譯時期被擦除, 因此泛型類並無本身獨有的Class類對象好比List<Integer>.class, 而只有List.class對象。

加強for循環

/**
 * 加強for循環
 * option: --collectioniter false
 */
public void forLoopTest() {
    String[] qingshanli = {"haha", "qingshan", "helloworld", "ceshi"};  
    List<String> list =  Arrays.asList(qingshanli);
    for (Object s : list) {
        System.out.println(s);
    }
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --collectioniter false

很明顯, 加強for循環的底層其實仍是經過迭代器來實現的, 這也就解釋了爲何加強for循環中不能進行增刪改操做。

lambda表達式

/**
 * lambda表達式
 * option: --decodelambdas false
 */
public void lambdaTest() {
    String[] qingshanli = {"haha", "qingshan", "helloworld", "ceshi"};  
    List<String> list =  Arrays.asList(qingshanli);
    // 使用lambda表達式以及函數操做
    list.forEach((str) -> System.out.print(str + "; "));
    // 在JDK8中使用雙冒號操做符
    list.forEach(System.out::println);  
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --decodelambdas false

這裏筆者經驗尚淺, 關於lambda表達式的實現原理暫不作闡述, 以避免誤人子弟, 歡迎有興趣的讀者在留言區一塊兒討論。

try-with-resources語句

/**
 * try-with-resources語句
 * option: --tryresources false
 */
public void tryWithResourcesTest() throws IOException {
    try (final StringWriter writer = new StringWriter();
         final StringWriter writer2 = new StringWriter()) {
        writer.write("This is qingshanli1");
        writer2.write("this is qingshanli2");
    }
}

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --tryresources false

在JDK7以前, 如IO流、數據庫鏈接等資源用完後, 都是經過finally代碼塊來釋放資源。而try-with-resources語法糖則幫咱們省去了釋放資源這一操做, 編譯器在解語法糖階段時會將它還原成原始的語法結構。

JDK10的局部變量類型推斷

/**
 * 局部變量類型推斷, JDK10開始支持
 * option: 不須要參數
 */
public void varTest() {
    //初始化局部變量  
    var string = "qingshanli";
    //初始化局部變量  
    var stringList = new ArrayList<String>();
    stringList.add("九幽陰靈,諸天神魔,以我血軀,奉爲犧牲。");
    stringList.add("三生七世,永墮閻羅,只爲情故,雖死不悔!");
    stringList.add("blog:http://www.cnblogs.com/qingshanli/");
    //加強for循環的索引
    for (var s : stringList){
        System.out.println(s);
    }
    //傳統for循環的局部變量定義
    for (var i = 0; i < stringList.size(); i++){
        System.out.println(stringList.get(i));
    }
}

JDK10環境下編譯: /home/qingshanli/Downloads/jdk-10.0.2/bin/javac CFRDecompilerDemo.java

命令行: java -jar cfr_0_132.jar CFRDecompilerDemo.class --collectioniter false

能夠看出, 局部變量類型推斷其實也是一個語法糖。在編譯過程的解語法糖階段, 會使用變量真正的類型來替代var類型。因此java由始至終是一種強類型語言, java中的var和弱類型語言JavaScript中的var是徹底不同的, 例以下圖 var i = "10" - 6 這樣的語法運算在JavaScript中能夠的, 而在Java語言中則不被容許。

另外目前已知的容許使用var聲明變量的幾個場景有初始化局部變量、加強for循環的索引、傳統for循環的局部變量定義。而諸如方法的形參、構造器的形參、方法的返回值類型、對象的成員變量、只進行定義而不初始化的變量等則不支持這種用法。對於後面的幾種不支持, 個人猜測是由於它們會被外部訪問而致使充滿了不肯定性, 舉個栗子, 好比對象的成員變量X, 被對象A訪問並賦值ArrayList類型, 被對象B訪問並賦值HashMap類型, 那麼問題來了, 對象A和對象B都是同一個類的實例, 這就產生了衝突, 此時虛擬機又如何區分這個對象的成員變量X究竟是什麼類型呢? 

源代碼

import java.util.*;
import java.io.*;

public class CFRDecompilerDemo {

    int x = 3;

    /**
     * 字符串拼接
     * option: --stringbuilder false
     */
    public void stringBuilderTest(int end) {
        char[] foo = new char[]{'@', 'a', '*'};
        char ch;
        int x = 0;
        while ((ch = foo[++x]) != '*') {
            System.out.println("" + x + ": " + ch);
        }
    }
    
    /**
     * 條件編譯
     * option: 不須要參數
     */
    public void ifCompilerTest() {
        if(false) {
            System.out.println("false if");
        }else {
            System.out.println("true else");
        }
    }
    
    /**
     * 斷言, JDK1.4開始支持
     * option: --sugarasserts false
     */
    public void assertTest(String s) {
        assert (!s.equals("Fred"));
        System.out.println(s);
    }
    
    /**
     * 枚舉與Switch語句
     * option: --decodeenumswitch false
     */
    public int switchEnumTest(EnumTest e) {
        switch (e) {
            case FOO:
                return 1;
            case BAP:
                return 2;
        }
        return 0;
    }
    
    /** 
     * 字符串與Switch語句
     * option: --decodestringswitch false
     */
   public int switchStringTest(String s) {
        switch (s) {
            default:
                System.out.println("Test");
                break;
            case "BB":  // BB and Aa have the same hashcode.
                return 12;
            case "Aa":
            case "FRED":
                return 13;
        }
        System.out.println("Here");
        return 0;
    }
    
    /**
     * 可變參數
     * option: --arrayiter false
     */
    public void varargsTest(String ... arr) {
        for (String s : arr) {
            System.out.println(s);
        }
    }
    
    /**
     * 自動裝箱/拆箱
     * option: --sugarboxing false
     */
    public Double autoBoxingTest(Integer i, Double d) {
        return d + i;
    }
    
    /**
     * 枚舉, JDK1.5開始支持
     * option: --sugarenums false
     */
    public enum EnumTest {
        FOO,
        BAR,
        BAP
    }
    
    /**
     * 內部類
     * option: --removeinnerclasssynthetics false
     */
    public void innerClassTest() {
        new InnerClass().getSum(6);
    }
    
    public class InnerClass {
        public int getSum(int y) {
            x += y;
            return x;
        }
    }
    
    /**
     * 泛型擦除
     * option: 
     */
    public void genericEraseTest() {
        List<String> list =  new ArrayList<String>();
    }
    
    /**
     * 加強for循環
     * option: --collectioniter false
     */
    public void forLoopTest() {
        String[] qingshanli = {"haha", "qingshan", "helloworld", "ceshi"};  
        List<String> list =  Arrays.asList(qingshanli);
        for (Object s : list) {
            System.out.println(s);
        }
    }
    
    /**
     * lambda表達式
     * option: --decodelambdas false
     */
    public void lambdaTest() {
        String[] qingshanli = {"haha", "qingshan", "helloworld", "ceshi"};  
        List<String> list =  Arrays.asList(qingshanli);
        // 使用lambda表達式以及函數操做
        list.forEach((str) -> System.out.print(str + "; "));
        // 在JDK8中使用雙冒號操做符
        list.forEach(System.out::println);  
    }
    
    /**
     * try-with-resources語句
     * option: --tryresources false
     */
    public void tryWithResourcesTest() throws IOException {
        try (final StringWriter writer = new StringWriter();
             final StringWriter writer2 = new StringWriter()) {
            writer.write("This is qingshanli1");
            writer2.write("this is qingshanli2");
        }
    }
    
    /**
     * 局部變量類型推斷, JDK10開始支持
     * option: 不須要參數
     */
    public void varTest() {
        //初始化局部變量  
        var string = "qingshanli";
        //初始化局部變量  
        var stringList = new ArrayList<String>();
        stringList.add("九幽陰靈,諸天神魔,以我血軀,奉爲犧牲。");
        stringList.add("三生七世,永墮閻羅,只爲情故,雖死不悔!");
        stringList.add("blog:http://www.cnblogs.com/qingshanli/");
        //加強for循環的索引
        for (var s : stringList){
            System.out.println(s);
        }
        //傳統for循環的局部變量定義
        for (var i = 0; i < stringList.size(); i++){
            System.out.println(stringList.get(i));
        }
    }
}
View Code

參數資料

Java的編譯原理

Java代碼的編譯與反編譯那些事兒-HollisChuang's Blog

我反編譯了Java 10的本地變量類型推斷-HollisChuang's Blog

Java中的Switch對整型、字符型、字符串型的具體實現細節-HollisChuang's Blo...

一些防止java代碼被反編譯的方法

做者:張小凡
出處:https://www.cnblogs.com/qingshanli/ 本文版權歸做者和博客園共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利。若是以爲還有幫助的話,能夠點一下右下角的【推薦】。

相關文章
相關標籤/搜索