Java 中12個常見的語法糖
本文從
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
這樣的語法。需要在編譯階段通過型別擦除的方式進行解語法糖。
型別擦除的主要過程如下:
-
將所有的泛型引數用其最左邊界(最頂級的父型別)型別替換。
-
移除所有的型別引數。
以下程式碼:
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中的自動拆裝箱
因為這裡的裝箱和拆箱是自動進行的非人為轉換,所以就稱作為自動裝箱和拆箱。
原始型別
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"
,
"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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java 中的語法糖,真甜。Java
- JAVA語法糖和語法糖編譯Java編譯
- C#12中的Collection expressions(集合表示式語法糖)C#Express
- Java語法糖詳解Java
- Hollis原創|不瞭解這12個語法糖,別說你會JavaJava
- 語法糖
- 常見的Java基礎語法知識都有哪些?Java
- ObjC中語法糖的趣味應用OBJ
- Java基礎-try-with-resource語法糖Java
- C#中檢查null的語法糖C#Null
- vue 常用語法糖Vue
- Golang常用語法糖Golang
- 大話 語法糖
- 傳說中 VUE 的“語法糖”到底是啥?Vue
- vue.js語法糖Vue.js
- SG :一個簡單的PHP語法糖擴充套件PHP套件
- 細數Java的語法糖(一): 用於字串拼接的 "+" 運算子Java字串
- 細數Java的語法糖(一): 用於字串拼接的 “+” 運算子Java字串
- C#中檢查null的語法糖,非常實用C#Null
- JavaScript 中一顆有毒的語法糖JavaScript
- Kotlin的語法糖(一)基礎篇Kotlin
- Vue v-model語法糖Vue
- 常見12個python面試題整理Python面試題
- java中list的常見問題。Java
- java集合中的常見考點Java
- Java開發工程師進階篇 - Java中隱藏的一些語法糖,你zao嗎?Java工程師
- C# 中居然也有切片語法糖,太厲害了C#
- CSS(1)基礎語法、常見屬性CSS
- C# 語法糖 ver2C#
- Golang接收者方法語法糖Golang
- 重新認識 async/await 語法糖AI
- angular 新語法糖學習一Angular
- java常見語句之迴圈體Java
- Java語法糖: 使用 try-with-resources 語句安全地釋放資源Java
- python語言幾個常見函式的使用Python函式
- Vue3.2 setup語法糖總結Vue
- java中的try-with-resource語法Java
- 【Vue3.0】關於 script setup 語法糖的用法Vue