歡迎光臨
每天分享高質量文章

在Java中12個常見的語法糖

作者 l Hollis

來源 l Hollis(ID:hollischuang)

 

本文從 Java 編譯原理角度,深入位元組碼及 class 檔案,抽絲剝繭,瞭解 Java 中的語法糖原理及用法,幫助大家在學會如何使用 Java 語法糖的同時,瞭解這些語法糖背後的原理

 

語法糖

語法糖(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或者使用純整數常量,但這也不是很差。

 

 

糖塊二、 泛型

我們都知道,很多語言都是支持泛型的,但是很多人不知道的是,不同的編譯器對於泛型的處理方式是不同的。

通常情況下,一個編譯器處理泛型有兩種方式:Code specialization和Code sharing。

C++和C#是使用Code specialization的處理機制,而Java使用的是Code sharing的機制。

Code sharing方式為每個泛型型別創建唯一的位元組碼表示,並且將該泛型型別的實體都映射到這個唯一的位元組碼表示上。將多種泛型類形實體映射到唯一的位元組碼表示是通過型別擦除(type erasue)實現的。

也就是說,對於Java虛擬機來說,他根本不認識Map map這樣的語法。需要在編譯階段通過型別擦除的方式進行解語法糖。

型別擦除的主要過程如下:

  •  1.將所有的泛型引數用其最左邊界(最頂級的父型別)型別替換。

  •  2.移除所有的型別引數。

以下代碼:

Map<StringString> map = new HashMap<StringString>();
map.put(“name”“hollis”);
map.put(“wechat”“Hollis”);
map.put(“blog”“www.hollischuang.com”);


解語法糖之後會變成:

Map map = new HashMap();
map.put(“name”“hollis”);
map.put(“wechat”“Hollis”);
map.put(“blog”“www.hollischuang.com”);


以下代碼:

public static extends Comparable> A max(Collection xs) {
Iterator
 xi = xs.iterator();
A w = xi.next();
while (xi.hasNext()) {
A x = xi.next();
if (w.compareTo(x) 0)
w = x;
}
return w;
}


型別擦除後會變成:

 public static Comparable max(Collection xs){
Iterator xi = xs.iterator();
Comparable w = (Comparable)xi.next();
while(xi.hasNext())
{
Comparable x = (Comparable)xi.next();
if(w.compareTo(x) 0
)
w = x;
}
return w;
}


虛擬機中沒有泛型,只有普通類和普通方法,所有泛型類的型別引數在編譯時都會被擦除,泛型類並沒有自己獨有的Class類物件。比如並不存在List.class或是List.class,而只有List.class。

 

糖塊三、 自動裝箱與拆箱

自動裝箱就是Java自動將原始型別值轉換成對應的物件,比如將int的變數轉換成Integer物件,這個過程叫做裝箱,反之將Integer物件轉換成int型別值,這個過程叫做拆箱。參考:一文讀懂什麼是Java中的自動拆裝箱

因為這裡的裝箱和拆箱是自動進行的非人為轉換,所以就稱作為自動裝箱和拆箱。

原始型別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方法實現的。

 

糖塊四 、 方法變長引數

可變引數(variable arguments)是在Java 1.5中引入的一個特性。它允許一個方法把任意數量的值作為引數。

看下以下可變引數代碼,其中print方法接收可變引數:

public static void main(String[] args)
{
print(“Holis”“公眾號:Hollis”“博客:www.hollischuang.com”“QQ:907607222”);
}

public static void print(String… strs)
{
for (int i = 0; i     {
System.out.println(strs[i]);
}
}


反編譯後代碼:

 public static void main(String args[])
{
print(new String[] {
“Holis”“\u516C\u4F17\u53F7:Hollis”“\u535A\u5BA2\uFF1Awww.hollischuang.com”“QQ\uFF1A907607222”
});
}

public static transient void print(String strs[])
{
for(int i = 0; i         System.out.println(strs[i]);

}


從反編譯後代碼可以看出,可變引數在被使用的時候,他首先會創建一個陣列,陣列的長度就是呼叫該方法是傳遞的引數的個數,然後再把引數值全部放到這個陣列當中,然後再把這個陣列作為引數傳遞到被呼叫的方法中。

 

糖塊五 、 列舉

Java SE5提供了一種新的型別-Java的列舉型別,關鍵字enum可以將一組具名的值的有限集合創建為一種新的型別,而這些具名的值可以作為常規的程式組件使用,這是一種非常有用的功能。參考:Java的列舉型別用法介紹

要想看原始碼,首先得有一個類吧,那麼列舉型別到底是什麼類呢?是enum嗎?

答案很明顯不是,enum就和class一樣,只是一個關鍵字,他並不是一個類。

那麼列舉是由什麼類維護的呢,我們簡單的寫一個列舉:

public enum t {
SPRING,SUMMER;
}


然後我們使用反編譯,看看這段代碼到底是怎麼實現的,反編譯後代碼內容如下:

public final class T extends Enum
{
private T(String s, int i)
{
super(s, i);
}
public static T[] values()
{
T at[];
int i;
T at1[];
System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
return at1;
}

public static T valueOf(String s)
{
return (T)Enum.valueOf(demo/T, s);
}

public static final T SPRING;
public static final T SUMMER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T(“SPRING”0);
SUMMER = new T(“SUMMER”1);
ENUM$VALUES = (new T[] {
SPRING, SUMMER
});
}
}


通過反編譯後代碼我們可以看到,public final class T extends Enum,說明,該類是繼承了Enum類的,同時final關鍵字告訴我們,這個類也是不能被繼承的。

當我們使用enmu來定義一個列舉型別的時候,編譯器會自動幫我們創建一個final型別的類繼承Enum類,所以列舉型別不能被繼承。

 

糖塊六 、 內部類

內部類又稱為嵌套類,可以把內部類理解為外部類的一個普通成員。

內部類之所以也是語法糖,是因為它僅僅是一個編譯時的概念。

outer.java裡面定義了一個內部類inner,一旦編譯成功,就會生成兩個完全不同的.class檔案了,分別是outer.class和outer$inner.class。所以內部類的名字完全可以和它的外部類名字相同。

public class OutterClass {
private String userName;

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public static void main(String[] args) {

}

class InnerClass{
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
}


以上代碼編譯後會生成兩個class檔案:OutterClass$InnerClass.class 、OutterClass.class 。

當我們嘗試使用jad對OutterClass.class檔案進行反編譯的時候,命令列會打印以下內容:

Parsing OutterClass.class...
Parsing inner class OutterClass$InnerClass.class...
Generating OutterClass.jad

他會把兩個檔案全部進行反編譯,然後一起生成一個OutterClass.jad檔案。檔案內容如下:

public class OutterClass
{
class InnerClass
{
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
private String name;
final OutterClass this$0;

InnerClass()
{
this.this$0 = OutterClass.this;
super();
}
}

public OutterClass()
{
}
public String getUserName()
{
return userName;
}
public void setUserName(String userName){
this.userName = userName;
}
public static void main(String args1[])
{
}
private String userName;
}


 

糖塊七 、條件編譯

—般情況下,程式中的每一行代碼都要參加編譯。但有時候出於對程式代碼優化的考慮,希望只對其中一部分內容進行編譯,此時就需要在程式中加上條件,讓編譯器只對滿足條件的代碼進行編譯,將不滿足條件的代碼捨棄,這就是條件編譯。

如在C或CPP中,可以通過預處理陳述句來實現條件編譯。其實在Java中也可實現條件編譯。我們先來看一段代碼:

public class ConditionalCompilation {
public static void main(String[] args) {
final boolean DEBUG = true;
if(DEBUG) {
System.out.println(“Hello, DEBUG!”);
}

final boolean ONLINE = false;

if(ONLINE){
System.out.println(“Hello, ONLINE!”);
}
}
}


反編譯後代碼如下:

public class ConditionalCompilation
{

public ConditionalCompilation()
{
}

public static void main(String args[])
{
boolean DEBUG = true;
System.out.println(“Hello, DEBUG!”);
boolean ONLINE = false;
}
}


首先,我們發現,在反編譯後的代碼中沒有System.out.println(“Hello, ONLINE!”);,這其實就是條件編譯。

當if(ONLINE)為false的時候,編譯器就沒有對其內的代碼進行編譯。

所以,Java語法的條件編譯,是通過判斷條件為常量的if陳述句實現的。根據if判斷條件的真假,編譯器直接把分支為false的代碼塊消除。通過該方式實現的條件編譯,必須在方法體內實現,而無法在正整個Java類的結構或者類的屬性上進行條件編譯。

這與C/C++的條件編譯相比,確實更有局限性。在Java語言設計之初並沒有引入條件編譯的功能,雖有局限,但是總比沒有更強。

 

糖塊八 、 斷言

在Java中,assert關鍵字是從JAVA SE 1.4 引入的,為了避免和老版本的Java代碼中使用了assert關鍵字導致錯誤,Java在執行的時候預設是不啟動斷言檢查的(這個時候,所有的斷言陳述句都將忽略!)。

如果要開啟斷言檢查,則需要用開關-enableassertions或-ea來開啟。

看一段包含斷言的代碼:

public class AssertTest {
public static void main(String args[]{
int a = 1;
int b = 1;
assert a == b;
System.out.println(“公眾號:Hollis”);
assert a != b : “Hollis”;
System.out.println(“博客:www.hollischuang.com”);
}
}


反編譯後代碼如下:

public class AssertTest {
public AssertTest()
{
}
public static void main(String args[])
{
int a = 1;
int b = 1;
if(!$assertionsDisabled && a != b)
throw new AssertionError();
System.out.println(“\u516C\u4F17\u53F7\uFF1AHollis”);
if(!$assertionsDisabled && a == b)
{
throw new AssertionError(“Hollis”);
else
{
System.out.println(“\u535A\u5BA2\uFF1Awww.hollischuang.com”);
return;
}
}

static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desiredAssertionStatus();

}


很明顯,反編譯之後的代碼要比我們自己的代碼複雜的多。所以,使用了assert這個語法糖我們節省了很多代碼。

其實斷言的底層實現就是if語言,如果斷言結果為true,則什麼都不做,程式繼續執行,如果斷言結果為false,則程式丟擲AssertError來打斷程式的執行。

-enableassertions會設置$assertionsDisabled欄位的值。

 

糖塊九 、 數值字面量

在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 = {“Hollis”“公眾號:Hollis”“博客:www.hollischuang.com”};
for (String s : strs) {
System.out.println(s);
}
List<String> strList = ImmutableList.of(“Hollis”“公眾號:Hollis”“博客:www.hollischuang.com”);
for (String s : strList) {
System.out.println(s);
}
}


反編譯後代碼如下:

public static transient void main(String args[])
{
String strs[] = {
“Hollis”“\u516C\u4F17\u53F7\uFF1AHollis”“\u535A\u5BA2\uFF1Awww.hollischuang.com”
};
String args1[] = strs;
int i = args1.length;
for(int j = 0; j     {
String s = args1[j];
System.out.println(s);
}

List strList = ImmutableList.of(“Hollis”“\u516C\u4F17\u53F7\uFF1AHollis”“\u535A\u5BA2\uFF1Awww.hollischuang.com”);
String s;
for(Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s))
s = (String)iterator.next();

}


代碼很簡單,for-each的實現原理其實就是使用了普通的for迴圈和迭代器。

 

糖塊十一 、 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:\\hollischuang.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:\\ hollischuang.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:\\ hollischuang.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;
}
}


其實背後的原理也很簡單,那些我們沒有做的關閉資源的操作,編譯器都幫我們做了。

所以,再次印證了,語法糖的作用就是方便程式員的使用,但最終還是要轉成編譯器認識的語言。

 

糖塊十二、Lambda運算式

關於lambda運算式,有人可能會有質疑,因為網上有人說他並不是語法糖。其實我想糾正下這個說法。

Labmda運算式不是匿名內部類的語法糖,但是他也是一個語法糖。實現方式其實是依賴了幾個JVM底層提供的lambda相關api。

先來看一個簡單的lambda運算式。遍歷一個list:

public static void main(String… args{
List strList = ImmutableList.of(“Hollis”“公眾號:Hollis”“博客:www.hollischuang.com”);

strList.forEach( s -> { System.out.println(s); } );
}


為啥說他並不是內部類的語法糖呢,前面講內部類我們說過,內部類在編譯之後會有兩個class檔案,但是,包含lambda運算式的類編譯後只有一個檔案。

反編譯後代碼如下:

public static /* varargs */ void main(String … args) {
ImmutableList strList = ImmutableList.of((Object)“Hollis”, (Object)“\u516c\u4f17\u53f7\uff1aHollis”, (Object)“\u535a\u5ba2\uff1awww.hollischuang.com”);
strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(nullnullnull, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());
}

private static /* synthetic */ void lambda$main$0(String s) {
System.out.println(s);
}


可以看到,在forEach方法中,其實是呼叫了java.lang.invoke.LambdaMetafactory#metafactory方法,該方法的第四個引數implMethod指定了方法實現。可以看到這裡其實是呼叫了一個lambda$main$0方法進行了輸出。

再來看一個稍微複雜一點的,先對List進行過濾,然後再輸出:

public static void main(String… args{
List strList = ImmutableList.of(“Hollis”“公眾號:Hollis”“博客:www.hollischuang.com”);

List HollisList = strList.stream().filter(string -> string.contains(“Hollis”)).collect(Collectors.toList());

HollisList.forEach( s -> { System.out.println(s); } );
}


反編譯後代碼如下:

public static /* varargs */ void main(String … args) {
ImmutableList strList = ImmutableList.of((Object)“Hollis”, (Object)“\u516c\u4f17\u53f7\uff1aHollis”, (Object)“\u535a\u5ba2\uff1awww.hollischuang.com”);
List<Object> HollisList = strList.stream().filter((Predicate<String>)LambdaMetafactory.metafactory(nullnullnull, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList());
HollisList.forEach((Consumer<Object>)LambdaMetafactory.metafactory(nullnullnull, (Ljava/lang/Object;)V, lambda$main$1(java.lang.Object ), (Ljava/lang/Object;)V)());
}

private static /* synthetic */ void lambda$main$1(Object s) {
System.out.println(s);
}

private static /* synthetic */ boolean lambda$main$0(String string) {
return string.contains(“Hollis”);
}


兩個lambda運算式分別呼叫了lambda$main$1和lambda$main$0兩個方法。

所以,lambda運算式的實現其實是依賴了一些底層的api,在編譯階段,編譯器會把lambda運算式進行解糖,轉換成呼叫內部api的方式。

 

可能遇到的坑

泛型——當泛型遇到多載 

public class GenericTypes {

public static void method(List list) {
System.out.println(“invoke method(List list)”);
}

public static void method(List list) {
System.out.println(“invoke method(List list)”);
}
}


上面這段代碼,有兩個多載的函式,因為他們的引數型別不同,一個是List另一個是List,但是,這段代碼是編譯通不過的。因為我們前面講過,引數List和List編譯之後都被擦除了,變成了一樣的原生型別List,擦除動作導致這兩個方法的特征簽名變得一模一樣。

泛型——當泛型遇到catch 

泛型的型別引數不能用在Java異常處理的catch陳述句中。因為異常處理是由JVM在運行時刻來進行的。由於型別信息被擦除,JVM是無法區分兩個異常型別MyException>和MyException的

泛型—當泛型內包含靜態變數

public class StaticTest{
public static void main(String[] args){
GT gti = new GT();
gti.var=1;
GT gts = new GT();
gts.var=2;
System.out.println(gti.var);
}
}
class GT{
public static int var=0;
public void nothing(T x){}
}


以上代碼輸出結果為:2!由於經過型別擦除,所有的泛型類實體都關聯到同一份位元組碼上,泛型類的所有靜態變數是共享的。

自動裝箱與拆箱——物件相等比較

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。

只適用於自動裝箱。使用建構式創建物件不適用。

增強for迴圈

for (Student stu : students) {
if (stu.getId() == 2)
students.remove(stu);
}


會丟擲ConcurrentModificationException異常。

Iterator是工作在一個獨立的執行緒中,並且擁有一個 mutex 鎖。 Iterator被創建之後會建立一個指向原來物件的單鏈索引表,當原來的物件數量發生變化時,這個索引表的內容不會同步改變,所以當索引指標往後移動的時候就找不到要迭代的物件,所以按照 fail-fast 原則 Iterator 會馬上丟擲java.util.ConcurrentModificationException異常。參考:一不小心就讓Java開發者踩坑的fail-fast是個什麼鬼?

所以 Iterator 在工作的時候是不允許被迭代的物件被改變的。但你可以使用 Iterator 本身的方法remove()來刪除物件,Iterator.remove() 方法會在刪除當前迭代物件的同時維護索引的一致性。

 

總結

前面介紹了12種Java中常用的語法糖。所謂語法糖就是提供給開發人員便於開發的一種語法而已。

但是這種語法只有開發人員認識。要想被執行,需要進行解糖,即轉成JVM認識的語法。

當我們把語法糖解糖之後,你就會發現其實我們日常使用的這些方便的語法,其實都是一些其他更簡單的語法構成的。

有了這些語法糖,我們在日常開發的時候可以大大提升效率,但是同時也要避免過渡使用。使用之前最好瞭解下原理,避免掉坑。

赞(0)

分享創造快樂