天天看點

JAVA開發必看:12種Java中常用的文法糖深度解析

本文從 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<String, String> map這樣的文法。需要在編譯階段通過類型擦除的方式進行解文法糖。

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

1.将所有的泛型參數用其最左邊界(最頂級的父類型)類型替換。

2.移除所有的類型參數。

以下代碼:

Map<String, String> map = new HashMap<String, String>(); 
map.put("name", "hollis"); 
map.put("wechat", "Hollis"); 
map.put("blog", "www.dalbll.com"); 
      

解文法糖之後會變成:

Map map = new HashMap(); 
map.put("name", "hollis"); 
map.put("wechat", "Hollis"); 
map.put("blog", "www.dalbll.com"); 
      

以下代碼:

public static <A extends Comparable<A>> A max(Collection<A> xs) {
 Iterator<A> 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<String>.class或是List<Integer>.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", "公衆号:dalbll", "部落格:www.dalbll.com", "QQ:2274840169");
 }
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[] {
 "dalbll", "\u516C\u4F17\u53F7:Hollis", "\u535A\u5BA2\uFF1Awww.dalbll.com.com", "QQ\uFF1A907607222"
 });
}
public static transient void print(String strs[])
{
 for(int i = 0; i < strs.length; 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("公衆号:dalbll");
 assert a != b : "Hollis";
 System.out.println("部落格:www.dalbll.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("dalbll");
 } else
 {
 System.out.println("\u535A\u5BA2\uFF1Awww.dalbll.com");
 return;
 }
}
static final boolean $assertionsDisabled = !com/dalbll/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", "公衆号:dalbll", "部落格:www.dalbll.com"};
 for (String s : strs) {
 System.out.println(s);
 }
 List<String> strList = ImmutableList.of("Hollis", "公衆号:dalbll", "部落格:www.dalbll.com");
 for (String s : strList) {
 System.out.println(s);
 }
}
      

反編譯後代碼如下:

public static transient void main(String args[])
{
 String strs[] = {
 "dalbll", "\u516C\u4F17\u53F7\uFF1Adalbll", "\u535A\u5BA2\uFF1Awww.dalbll.com"
 };
 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("dalbll", "\u516C\u4F17\u53F7\uFF1Adalbll", "\u535A\u5BA2\uFF1Awww.dalbll.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<String> strList = ImmutableList.of("dalbll", "公衆号:dalbll", "部落格:www.dalbll.com");
 strList.forEach( s -> { System.out.println(s); } );
}
      

為啥說他并不是内部類的文法糖呢,前面講内部類我們說過,内部類在編譯之後會有兩個class檔案,但是,包含lambda表達式的類編譯後隻有一個檔案。

反編譯後代碼如下:

public static /* varargs */ void main(String ... args) {
 ImmutableList strList = ImmutableList.of((Object)"dalbll", (Object)"\u516c\u4f17\u53f7\uff1adalbll", (Object)"\u535a\u5ba2\uff1awww.dalbll.com");
 strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (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<String> strList = ImmutableList.of("dalbll", "公衆号:dalbll", "部落格:www.dalbll.com");
 List HollisList = strList.stream().filter(string -> string.contains("dalbll")).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.dalbll.com");
 List<Object> HollisList = strList.stream().filter((Predicate<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList());
 HollisList.forEach((Consumer<Object>)LambdaMetafactory.metafactory(null, null, null, (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("dalbll");
}
      

兩個lambda表達式分别調用了lambda$main$1和lambda$main$0兩個方法。是以,lambda表達式的實作其實是依賴了一些底層的api,在編譯階段,編譯器會把lambda表達式進行解糖,轉換成調用内部api的方式。

可能遇到的坑

泛型——當泛型遇到重載

public class GenericTypes {
 public static void method(List<String> list) { 
 System.out.println("invoke method(List<String> list)"); 
 } 
 public static void method(List<Integer> list) { 
 System.out.println("invoke method(List<Integer> list)"); 
 } 
}
      

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

泛型——當泛型遇到catch

泛型的類型參數不能用在Java異常處理的catch語句中。因為異常處理是由JVM在運作時刻來進行的。由于類型資訊被擦除,JVM是無法區分兩個異常類型MyException<String>和MyException<Integer>的

泛型——當泛型内包含靜态變量

public class StaticTest{
 public static void main(String[] args){
 GT<Integer> gti = new GT<Integer>();
 gti.var=1;
 GT<String> gts = new GT<String>();
 gts.var=2;
 System.out.println(gti.var);
 }
}
class GT<T>{
 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認識的文法。當我們把文法糖解糖之後,你就會發現其實我們日常使用的這些友善的文法,其實都是一些其他更簡單的文法構成的。有了這些文法糖,我們在日常開發的時候可以大大提升效率,但是同時也要避免過渡使用。使用之前最好了解下原理,避免掉坑。

寫在最後:

碼字不易看到最後了,那就點個關注呗,隻收藏不點關注的都是在耍流氓!

關注并私信我“架構”,免費送一些Java架構資料,先到先得!