語法糖
語法糖(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;
}
}
}
反編譯後內容如下:
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或者使用純整數常量,但這也不是很差。
語法糖之-自動裝箱與拆箱
自動裝箱就是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方法實現的。
注意
物件相等比較使用自動裝箱可能造成的問題
public class BoxingTest {
public static void main(String[] args) {
Integer a = 1000;
Integer b = 1000;
Integer c = 100;
Integer d = 100;
System.out.println("a == b is " + (a == b));
System.out.println(("c == d is " + (c == d)));
}
輸出結果
a == b is false
c == d is true
在Java 5中,在Integer的操作上引入了一個新功能來節省記憶體和提高效能。整型物件通過使用相同的物件引用實現了快取和重用。
適用於整數值區間-128 至 +127。
只適用於自動裝箱。使用建構函式建立物件不適用。
語法糖之-方法變長引數
可變引數(variable arguments)是在Java 1.5中引入的一個特性。它允許一個方法把任意數量的值作為引數。
看下以下可變引數程式碼,其中print方法接收可變引數:
public static void main(String[] args)
{
print("hello", "world", "123", "456");
}
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[] {
"hello", "world", "123", "456"
});
}
public static transient void print(String strs[])
{
for(int i = 0; i < strs.length; i++)
System.out.println(strs[i]);
}
從反編譯後程式碼可以看出,可變引數在被使用的時候,他首先會建立一個陣列,陣列的長度就是呼叫該方法是傳遞的實參的個數,然後再把引數值全部放到這個陣列當中,然後再把這個陣列作為引數傳遞到被呼叫的方法中。
語法糖之-數值字面量
在java 7中,數值字面量,不管是整數還是浮點數,都允許在數字之間插入任意多個下劃線。這些下劃線不會對字面量的數值產生影響,目的就是方便閱讀。
比如:
public class Test {
public static void main(String... args) {
int i = 10_000;
System.out.println(i);
}
}
反編譯後:
public class Test
{
public static void main(String[] args)
{
int i = 10000;
System.out.println(i);
}
}
反編譯後就是把_刪除了。也就是說 編譯器並不認識在數字字面量中的_,需要在編譯階段把他去掉。
語法糖之-for-each
增強for迴圈(for-each)相信大家都不陌生,日常開發經常會用到的,他會比for迴圈要少寫很多程式碼,那麼這個語法糖背後是如何實現的呢?
public static void main(String... args) {
String[] strs = {"hello", "world", "123", "456"};
for (String s : strs) {
System.out.println(s);
}
List<String> strList = ImmutableList.of("hello", "world", "123", "456");
for (String s : strList) {
System.out.println(s);
}
}
反編譯後程式碼如下:
public static transient void main(String args[])
{
String strs[] = {
"hello", "world", "123", "456"
};
String args1[] = strs;
int i = args1.length;
for(int j = 0; j < i; j++)
{
String s = args1[j];
System.out.println(s);
}
List strList = ImmutableList.of("hello", "world", "123", "456");
String s;
for(Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s))
s = (String)iterator.next();
}
程式碼很簡單,for-each的實現原理其實就是使用了普通的for迴圈和迭代器。
注意
使用增強fou可能遇到ConcurrentModificationException異常
for (Student stu : students) {
if (stu.getId() == 2)
students.remove(stu);
}
會丟擲ConcurrentModificationException異常。
Iterator是工作在一個獨立的執行緒中,並且擁有一個 mutex 鎖。 Iterator被建立之後會建立一個指向原來物件的單鏈索引表,當原來的物件數量發生變化時,這個索引表的內容不會同步改變,所以當索引指標往後移動的時候就找不到要迭代的物件,所以按照 fail-fast 原則 Iterator 會馬上丟擲java.util.ConcurrentModificationException異常。
所以 Iterator 在工作的時候是不允許被迭代的物件被改變的。但你可以使用 Iterator 本身的方法remove()來刪除物件,Iterator.remove() 方法會在刪除當前迭代物件的同時維護索引的一致性。
語法糖之-try-with-resource
Java裡,對於檔案操作IO流、資料庫連線等開銷非常昂貴的資源,用完之後必須及時通過close方法將其關閉,否則資源會一直處於開啟狀態,可能會導致記憶體洩露等問題。
關閉資源的常用方式就是在finally塊裡是釋放,即呼叫close方法。比如,我們經常會寫這樣的程式碼:
public static void main(String[] args) {
BufferedReader br = null;
try {
String line;
br = new BufferedReader(new FileReader("d:\\test.xml"));
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// handle exception
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException ex) {
// handle exception
}
}
}
從Java 7開始,jdk提供了一種更好的方式關閉資源,使用try-with-resources語句,改寫一下上面的程式碼,效果如下:
public static void main(String... args) {
try (BufferedReader br = new BufferedReader(new FileReader("d:\\ test.xml"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// handle exception
}
}
看,這簡直是一大福音啊,雖然我之前一般使用IOUtils去關閉流,並不會使用在finally中寫很多程式碼的方式,但是這種新的語法糖看上去好像優雅很多呢。看下他的背後:
public static transient void main(String args[])
{
BufferedReader br;
Throwable throwable;
br = new BufferedReader(new FileReader("d:\\ test.xml"));
throwable = null;
String line;
try
{
while((line = br.readLine()) != null)
System.out.println(line);
}
catch(Throwable throwable2)
{
throwable = throwable2;
throw throwable2;
}
if(br != null)
if(throwable != null)
try
{
br.close();
}
catch(Throwable throwable1)
{
throwable.addSuppressed(throwable1);
}
else
br.close();
break MISSING_BLOCK_LABEL_113;
Exception exception;
exception;
if(br != null)
if(throwable != null)
try
{
br.close();
}
catch(Throwable throwable3)
{
throwable.addSuppressed(throwable3);
}
else
br.close();
throw exception;
IOException ioexception;
ioexception;
}
}
其實背後的原理也很簡單,那些我們沒有做的關閉資源的操作,編譯器都幫我們做了。所以,再次印證了,語法糖的作用就是方便程式設計師的使用,但最終還是要轉成編譯器認識的語言。
總結
所謂語法糖就是提供給開發人員便於開發的一種語法而已。但是這種語法只有開發人員認識。要想被執行,需要進行解糖,即轉成JVM認識的語法。當我們把語法糖解糖之後,你就會發現其實我們日常使用的這些方便的語法,其實都是一些其他更簡單的語法構成的。
結語
歡迎關注微信公眾號『碼仔zonE』,專注於分享Java、雲端計算相關內容,包括SpringBoot、SpringCloud、微服務、Docker、Kubernetes、Python等領域相關技術乾貨,期待與您相遇!