概述
編譯器是一種計算機程序, 它主要的目的是將便於人編寫、閱讀、維護的高級計算機語言所寫的源代碼程序, 翻譯為計算機能解讀、運行的低階機器語言的程序, 即可執行文件。而 javac 就是java語言中的編譯器, 它用於將 .java 文件轉換成JVM能識別的 .class 字節碼文件, 反編譯則是將 .class 文件轉換成 .java 文件。
語法糖(Syntactic sugar),也譯為糖衣語法,是由英國計算機科學家彼得·蘭丁發明的一個術語,指計算機語言中添加的某種語法,這種語法對語言的功能沒有影響,但是更方便程序員使用。語法糖讓程序更加簡潔,有更高的可讀性。
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.class

很明顯, 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異常來打斷程序的執行。
枚舉與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還支持byte、short、int、char這幾種基本數據類型, 其中支持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)); } } }
參數資料
Java代碼的編譯與反編譯那些事兒-HollisChuang's Blog
我反編譯了Java 10的本地變量類型推斷-HollisChuang's Blog
Java中的Switch對整型、字符型、字符串型的具體實現細節-HollisChuang's Blo...
作者:張小凡
出處:https://www.cnblogs.com/qingshanli/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。如果覺得還有幫助的話,可以點一下右下角的【推薦】。
