Java 中12個常見的語法糖

喝水會長肉發表於2021-12-10

本文從  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 自身原本就支援基本型別。比如  intchar 等。

對於  int 型別,直接進行數值的比較。對於  char 型別則是比較其  ASCII碼。

所以,對於編譯器來說, switch 中其實只能使用整型,任何型別的比較都要轉換成整型。比如  byteshortchar(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" , " ) ;

解語法糖之後會變成:

Map map 
= 
new 
HashMap
(
)
;

map . put ( "name" , "hollis" ) ;
map . put ( "wechat" , "Hollis" ) ;
map . put ( "blog" , " ) ;

以下程式碼:


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中的自動拆裝箱

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

原始型別  byteshortcharintlongfloatdouble 和  boolean 對應的封裝類為  ByteShortCharacterIntegerLongFloatDoubleBoolean

先來看個自動裝箱的程式碼:


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" , "java交流學習" , "領取學習資料" , "Q菌:737251827" ) ;
    }

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\uFF1A , "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 ( "java學習交流" ) ;
       assert a != b : "Hollis" ;
       System .out . println ( "q菌:737251827" ) ;
    }
}

反編譯後程式碼如下:


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\uFF1A ) ;
        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" , "java學習交流" , "q菌737251827" } ;
    for ( String s : strs ) {
       System .out . println (s ) ;
    }
   List <String > strList = ImmutableList . of ( "Hollis" , "java" , "mysql" ) ;
    for ( String s : strList ) {
       System .out . println (s ) ;
    }
}

反編譯後程式碼如下:


public 
static transient 
void 
main
(
String args
[
]
)

{
   String strs [ ] = {
        "dalbll" , "\u516C\u4F17\u53F7\uFF1Adalbll" , "\u535A\u5BA2\uFF1A
    } ;
   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\uFF1A ) ;
   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" , "java學習交流" , "javaEE" ) ;

   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\uff1a ) ;
   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" , "java學習交流" , "javaEE" ) ;

   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\uff1a ) ;
   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<String> 另一個是  List<Integer>,但是,這段程式碼是編譯通不過的。因為我們前面講過,引數  List<String> 和  List<Integer> 編譯之後都被擦除了,變成了一樣的原生型別  List,擦除動作導致這兩個方法的特徵簽名變得一模一樣。

泛型——當泛型遇到catch

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

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


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 ) ;
    }
}
//java學習交流:737251827  進入可領取學習資源及對十年開發經驗大佬提問,免費解答!
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 t

輸出結果:

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 認識的語法。

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

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



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70010294/viewspace-2846977/,如需轉載,請註明出處,否則將追究法律責任。

相關文章