我們在日常開發(fā)中經(jīng)常會使用到諸如泛型、自動拆箱和裝箱、內(nèi)部類、增強(qiáng) for 循環(huán)、try-with-resources 語法、lambda 表達(dá)式等,我們只覺得用的很爽,因為這些特性能夠幫助我們減輕開發(fā)工作量;但我們未曾認(rèn)真研究過這些特性的本質(zhì)是什么 語法糖在聊之前我們需要先了解一下 語法糖指的是計算機(jī)語言中添加的某種語法,這種語法對語言的功能并沒有影響,但是更方便程序員使用。因為 Java 代碼需要運行在 JVM 中,JVM 是并不支持語法糖的,語法糖在程序編譯階段就會被還原成簡單的基礎(chǔ)語法結(jié)構(gòu),這個過程就是 下面我們就來認(rèn)識一下 Java 中的這些語法糖 泛型泛型是一種語法糖。在 JDK1.5 中,引入了泛型機(jī)制,但是泛型機(jī)制的本身是通過 List<Integer> aList = new ArrayList(); List<String> bList = new ArrayList(); System.out.println(aList.getClass() == bList.getClass());
如下圖所示 無法將一個 Integer 類型的數(shù)據(jù)放在 自動拆箱和自動裝箱自動拆箱和自動裝箱是一種語法糖,它說的是八種基本數(shù)據(jù)類型的包裝類和其基本數(shù)據(jù)類型之間的自動轉(zhuǎn)換。簡單的說,裝箱就是自動將基本數(shù)據(jù)類型轉(zhuǎn)換為 我們先來了解一下基本數(shù)據(jù)類型的包裝類都有哪些 也就是說,上面這些基本數(shù)據(jù)類型和包裝類在進(jìn)行轉(zhuǎn)換的過程中會發(fā)生自動裝箱/拆箱,例如下面代碼 Integer integer = 66; // 自動拆箱 int i1 = integer; // 自動裝箱
上面代碼中的 integer 對象會使用基本數(shù)據(jù)類型來進(jìn)行賦值,而基本數(shù)據(jù)類型 i1 卻把它賦值給了一個對象類型,一般情況下是不能這樣操作的,但是編譯器卻允許我們這么做,這其實就是一種語法糖。這種語法糖使我們方便我們進(jìn)行數(shù)值運算,如果沒有語法糖,在進(jìn)行數(shù)值運算時,你需要先將對象轉(zhuǎn)換成基本數(shù)據(jù)類型,基本數(shù)據(jù)類型同時也需要轉(zhuǎn)換成包裝類型才能使用其內(nèi)置的方法,無疑增加了代碼冗余。
其實這背后的原理是編譯器做了優(yōu)化。將基本類型賦值給包裝類其實是調(diào)用了包裝類的
而包裝類賦值給基本類型就是調(diào)用了包裝類的 xxxValue() 方法拿到基本數(shù)據(jù)類型后再進(jìn)行賦值。 Integer i1 = new Integer(1).intValue();
我們使用 javap -c 反編譯一下上面的自動裝箱和自動拆箱來驗證一下 可以看到,在 Code 2 處調(diào)用 在 Code 7 處調(diào)用了 枚舉我們在日常開發(fā)中經(jīng)常會使用到 但是在 Java 字節(jié)碼結(jié)構(gòu)中,并沒有枚舉類型。枚舉只是一個語法糖,在編譯完成后就會被編譯成一個普通的類,也是用 Class 修飾。這個類繼承于 java.lang.Enum,并被 final 關(guān)鍵字修飾。 我們舉個例子來看一下 public enum School { STUDENT, TEACHER; }
這是一個 School 的枚舉,里面包括兩個字段,一個是 STUDENT ,一個是 TEACHER,除此之外并無其他。 下面我們使用 從圖中我們可以看到,枚舉其實就是一個繼承于 除此之外,編譯器還會為我們生成兩個方法,
用法如下 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()); } } }
內(nèi)部類內(nèi)部類是 Java 一個 Java 語言中之所以引入內(nèi)部類,是因為有些時候一個類只想在一個類中有用,不想讓其在其他地方被使用,也就是對外隱藏內(nèi)部細(xì)節(jié)。 內(nèi)部類其實也是一個語法糖,因為其只是一個編譯時的概念,一旦編譯完成,編譯器就會為內(nèi)部類生成一個單獨的class 文件,名為 outer$innter.class。 下面我們就根據(jù)一個示例來驗證一下。 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 文件,一個是 我們來看一下內(nèi)部類編譯后的結(jié)果 如上圖所示,內(nèi)部類經(jīng)過編譯后的 linkOuter() 方法會生成一個指向外部類的 this 引用,這個引用就是連接外部類和內(nèi)部類的引用。 變長參數(shù)變長參數(shù)也是一個比較小眾的用法,所謂變長參數(shù),就是方法可以接受長度不定確定的參數(shù)。一般我們開發(fā)不會使用到變長參數(shù),而且變長參數(shù)也不推薦使用,它會使我們的程序變的難以處理。但是我們有必要了解一下變長參數(shù)的特性。 其基本用法如下 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"); } }
變長參數(shù)也是一種語法糖,那么它是如何實現(xiàn)的呢?我們可以猜測一下其內(nèi)部應(yīng)該是由數(shù)組構(gòu)成,否則無法接受多個值,那么我們反編譯看一下是不是由數(shù)組實現(xiàn)的。 可以看到,printMessage() 的參數(shù)就是使用了一個數(shù)組來接收,所以千萬別被變長參數(shù) 變長參數(shù)特性是在 JDK 1.5 中引入的,使用變長參數(shù)有兩個條件,一是變長的那一部分參數(shù)具有相同的類型,二是變長參數(shù)必須位于方法參數(shù)列表的最后面。 增強(qiáng) for 循環(huán)為什么有了普通的 for 循環(huán)后,還要有增強(qiáng) for 循環(huán)呢?想一下,普通 for 循環(huán)你不是需要知道遍歷次數(shù)?每次還需要知道數(shù)組的索引是多少,這種寫法明顯有些繁瑣。增強(qiáng) for 循環(huán)與普通 for 循環(huán)相比,功能更強(qiáng)并且代碼更加簡潔,你無需知道遍歷的次數(shù)和數(shù)組的索引即可進(jìn)行遍歷。 增強(qiáng) for 循環(huán)的對象要么是一個數(shù)組,要么實現(xiàn)了 Iterable 接口。這個語法糖主要用來對數(shù)組或者集合進(jìn)行遍歷,其在循環(huán)過程中不能改變集合的大小。 public static void main(String[] args) { String[] params = new String[]{"hello","world"}; //增強(qiáng)for循環(huán)對象為數(shù)組 for(String str : params){ System.out.println(str); } List<String> lists = Arrays.asList("hello","world"); //增強(qiáng)for循環(huán)對象實現(xiàn)Iterable接口 for(String str : lists){ System.out.println(str); } }
經(jīng)過編譯后的 class 文件如下 public static void main(String[] args) { String[] params = new String[]{"hello", "world"}; String[] lists = params; int var3 = params.length; //數(shù)組形式的增強(qiáng)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(); //實現(xiàn)Iterable接口的增強(qiáng)for使用iterator接口進(jìn)行遍歷 while(var7.hasNext()) { String var8 = (String)var7.next(); System.out.println(var8); } }
如上代碼所示,如果對數(shù)組進(jìn)行增強(qiáng) for 循環(huán)的話,其內(nèi)部還是對數(shù)組進(jìn)行遍歷,只不過語法糖把你忽悠了,讓你以一種更簡潔的方式編寫代碼。 而對繼承于 Iterator 迭代器進(jìn)行增強(qiáng) for 循環(huán)遍歷的話,相當(dāng)于是調(diào)用了 Iterator 的 Switch 支持字符串和枚舉
如下代碼所示 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; } } }
我們反編譯一下,看看我們的猜想是否正確 根據(jù)字節(jié)碼可以看到,進(jìn)行 switch 的實際是 hashcode 進(jìn)行判斷,然后通過使用 equals 方法進(jìn)行比較,因為字符串有可能會產(chǎn)生哈希沖突的現(xiàn)象。 條件編譯這個又是讓小伙伴們摸不著頭腦了,什么是條件編譯呢?其實,如果你用過 C 或者 C++ 你就知道可以通過預(yù)處理語句來實現(xiàn)條件編譯。
一般情況下,源程序中所有的行都參加編譯。但有時希望對其中一部分內(nèi)容只在滿足一定條件下才進(jìn)行編譯,即對一部分內(nèi)容指定編譯條件,這就是 #define DEBUG #IFDEF DEBUUG /* code block 1 */ #ELSE /* code block 2 */ #ENDIF
但是在 Java 中沒有預(yù)處理和宏定義這些內(nèi)容,那么我們想實現(xiàn)條件編譯,應(yīng)該怎樣做呢? 使用 final + if 的組合就可以實現(xiàn)條件編譯了。如下代碼所示 public static void main(String[] args) { final boolean DEBUG = true; if (DEBUG) { System.out.println("Hello, world!"); } else { System.out.println("nothing"); } }
這段代碼會發(fā)生什么?我們反編譯看一下 我們可以看到,我們明明是使用了 if ...else 語句,但是編譯器卻只為我們編譯了 DEBUG = true 的條件, 所以,Java 語法的條件編譯,是通過判斷條件為常量的 if 語句實現(xiàn)的,編譯器不會為我們編譯分支為 false 的代碼。 斷言你在 Java 中使用過斷言作為日常的判斷條件嗎? 斷言:也就是所謂的 //這個成員變量的值可以變,但最終必須還是回到原值5 static int i = 5; public static void main(String[] args) { assert i == 5; System.out.println("如果斷言正常,我就被打印"); }
如果要開啟斷言檢查,則需要用開關(guān) -enableassertions 或 -ea 來開啟。其實斷言的底層實現(xiàn)就是 if 判斷,如果斷言結(jié)果為 true,則什么都不做,程序繼續(xù)執(zhí)行,如果斷言結(jié)果為 false,則程序拋出 AssertError 來打斷程序的執(zhí)行。 assert 斷言就是通過對布爾標(biāo)志位進(jìn)行了一個 if 判斷。 try-with-resourcesJDK 1.7 開始,java引入了 try-with-resources 聲明,將 try-catch-finally 簡化為 try-catch,這其實是一種 如下代碼所示 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 經(jīng)過編譯后還是使用的 try...catch...finally 語句,只不過這部分工作由編譯器替我們做了,這樣能讓我們的代碼更加簡潔,從而消除樣板代碼。 字符串相加這個想必大家應(yīng)該都知道,字符串的拼接有兩種,如果能夠在編譯時期確定拼接的結(jié)果,那么使用 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; } }
上面這段代碼就包含了兩種字符串拼接的結(jié)果,我們反編譯看一下 首先來看一下 s1 ,s1 因為 = 號右邊是兩個常量,所以兩個字符串拼接會被直接優(yōu)化成為 而由于 s5 進(jìn)行拼接的兩個對象在編譯期不能判定其拼接結(jié)果,所以會直接使用 StringBuilder 進(jìn)行拼接。
學(xué)習(xí)語法糖的意義互聯(lián)網(wǎng)時代,有很多標(biāo)新立異的想法和框架層出不窮,但是,我們對于學(xué)習(xí)來說應(yīng)該抓住技術(shù)的核心。然而,軟件工程是一門協(xié)作的藝術(shù),對于工程來說如何提高工程質(zhì)量,如何提高工程效率也是我們要關(guān)注的,既然這些語法糖能輔助我們以更好的方式編寫備受歡迎的代碼,我們程序員為什么要
語法糖也是一種進(jìn)步,這就和你寫作文似的,大白話能把故事講明白的它就沒有語言優(yōu)美、酣暢淋漓的把故事講生動的更令人喜歡。
有完整的Java初級,高級對應(yīng)的學(xué)習(xí)路線和資料!專注于java開發(fā)。分享java基礎(chǔ)、原理性知識、JavaWeb實戰(zhàn)、spring全家桶、設(shè)計模式、分布式及面試資料、開源項目,助力開發(fā)者成長!
|
|