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", "Soinice");  
map.put("wechat", "Full-Stack");  
map.put("blog", "https://blog.csdn.net/Soinice");  

解語法糖之後會變成:

Map map = new HashMap();  
map.put("name", "Soinice");  
map.put("wechat", "Full-Stack");  
map.put("blog", "https://blog.csdn.net/Soinice");  

以下代碼:

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 類型值,這個過程叫做拆箱。

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

原始類型 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("Soinice", "公衆號:Full-Stack", "博客:https://blog.csdn.net/Soinice", "QQ:123456");
    }

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[] {
        "", "\u516C\u4F17\u53F7:Full-Stack", "\u535A\u5BA2\uFF1Ahttps://blog.csdn.net/Soinice", "QQ\uFF1A123456"
    });
}

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

}

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

枚舉

Java SE5 提供了一種新的類型 -Java 的枚舉類型,關鍵字 enum 可以將一組具名的值的有限集合創建爲一種新的類型,而這些具名的值可以作爲常規的程序組件使用,這是一種非常有用的功能。

要想看源碼,首先得有一個類吧,那麼枚舉類型到底是什麼類呢?是 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("公衆號:Full-Stack");
        assert a != b : "Hollis";
        System.out.println("博客:https://blog.csdn.net/Soinice");
    }
}

反編譯後代碼如下:

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\uFF1AFull-Stack");
    if(!$assertionsDisabled && a == b)
    {
        throw new AssertionError("Hollis");
    } else
    {
        System.out.println("\u535A\u5BA2\uFF1Ahttps://blog.csdn.net/Soinice");
        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", "公衆號:Full-Stack", "博客:https://blog.csdn.net/Soinice"};
    for (String s : strs) {
        System.out.println(s);
    }
    List<String> strList = ImmutableList.of("Full-Stack", "公衆號:Hollis", "博客:https://blog.csdn.net/Soinice");
    for (String s : strList) {
        System.out.println(s);
    }
}

反編譯後代碼如下:

public static transient void main(String args[])
{
    String strs[] = {
        "Full-Stack", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Ablog.csdn.net/Soinice"
    };
    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("Full-Stack", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Ablog.csdn.net/Soinice");
    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:\\Soinice.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:\\ Soinice.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:\\ Soinice.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("Hollis", "公衆號:Full-Stack", "博客:https://blog.csdn.net/Soinice");

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

爲啥說他並不是內部類的語法糖呢,前面講內部類我們說過,內部類在編譯之後會有兩個 class 文件,但是,包含 lambda 表達式的類編譯後只有一個文件。

反編譯後代碼如下:

public static /* varargs */ void main(String ... args) {
    ImmutableList strList = ImmutableList.of((Object)"Full-Stack", (Object)"\u516c\u4f17\u53f7\uff1aFull-Stack", (Object)"\u535a\u5ba2\uff1ahttps://blog.csdn.net/Soinice");
    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("Hollis", "公衆號:Full-Stack", "博客:https://blog.csdn.net/Soinice");

    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)"Full-Stack", (Object)"\u516c\u4f17\u53f7\uff1aFull-Stack", (Object)"\u535a\u5ba2\uff1ahttps://blog.csdn.net/Soinice");
    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("Hollis");
}

兩個 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 異常。

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

總結

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

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

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章