在Java中12個常見的語法糖
本文從 Java 編譯原理角度,深入字節(jié)碼及 class 文件,抽絲剝繭,了解 Java 中的語法糖原理及用法,幫助大家在學(xué)會如何使用 Java 語法糖的同時,了解這些語法糖背后的原理
語法糖
語法糖(Syntactic Sugar),也稱糖衣語法,是由英國計算機學(xué)家 Peter.J.Landin 發(fā)明的一個術(shù)語,指在計算機語言中添加的某種語法,這種語法對語言的功能并沒有影響,但是更方便程序員使用。簡而言之,語法糖讓程序更加簡潔,有更高的可讀性。
有意思的是,在編程領(lǐng)域,除了語法糖,還有語法鹽和語法糖精的說法,篇幅有限這里不做擴展了。
我們所熟知的編程語言中幾乎都有語法糖。作者認為,語法糖的多少是評判一個語言夠不夠牛逼的標準之一。
很多人說Java是一個“低糖語言”,其實從Java 7開始Java語言層面上一直在添加各種糖,主要是在“Project Coin”項目下研發(fā)。盡管現(xiàn)在Java有人還是認為現(xiàn)在的Java是低糖,未來還會持續(xù)向著“高糖”的方向發(fā)展。
解語法糖
前面提到過,語法糖的存在主要是方便開發(fā)人員使用。但其實,Java虛擬機并不支持這些語法糖。這些語法糖在編譯階段就會被還原成簡單的基礎(chǔ)語法結(jié)構(gòu),這個過程就是解語法糖。
說到編譯,大家肯定都知道,Java語言中,javac命令可以將后綴名為.java的源文件編譯為后綴名為.class的可以運行于Java虛擬機的字節(jié)碼。
如果你去看com.sun.tools.javac.main.JavaCompiler的源碼,你會發(fā)現(xiàn)在compile()中有一個步驟就是調(diào)用desugar(),這個方法就是負責(zé)解語法糖的實現(xiàn)的。
Java 中最常用的語法糖主要有泛型、變長參數(shù)、條件編譯、自動拆裝箱、內(nèi)部類等。本文主要來分析下這些語法糖背后的原理。一步一步剝?nèi)ヌ且?,看看其本質(zhì)。
糖塊一、 switch 支持 String 與枚舉
前面提到過,從Java 7 開始,Java語言中的語法糖在逐漸豐富,其中一個比較重要的就是Java 7中switch開始支持String。
在開始coding之前先科普下,Java中的swith自身原本就支持基本類型。比如int、char等。
對于int類型,直接進行數(shù)值的比較。對于char類型則是比較其ascii碼。
所以,對于編譯器來說,switch中其實只能使用整型,任何類型的比較都要轉(zhuǎn)換成整型。比如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;
- }
- }
- }
反編譯后內(nèi)容如下:
- 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()方法來實現(xiàn)的。還好hashCode()方法返回的是int,而不是long。
仔細看下可以發(fā)現(xiàn),進行switch的實際是哈希值,然后通過使用equals方法比較進行安全檢查,這個檢查是必要的,因為哈希可能會發(fā)生碰撞。因此它的性能是不如使用枚舉進行switch或者使用純整數(shù)常量,但這也不是很差。
糖塊二、 泛型
我們都知道,很多語言都是支持泛型的,但是很多人不知道的是,不同的編譯器對于泛型的處理方式是不同的。
通常情況下,一個編譯器處理泛型有兩種方式:Code specialization和Code sharing。
C++和C#是使用Code specialization的處理機制,而Java使用的是Code sharing的機制。
Code sharing方式為每個泛型類型創(chuàng)建唯一的字節(jié)碼表示,并且將該泛型類型的實例都映射到這個唯一的字節(jié)碼表示上。將多種泛型類形實例映射到唯一的字節(jié)碼表示是通過類型擦除(type erasue)實現(xiàn)的。
也就是說,對于Java虛擬機來說,他根本不認識Map
類型擦除的主要過程如下:
1.將所有的泛型參數(shù)用其最左邊界(最***的父類型)類型替換。
2.移除所有的類型參數(shù)。
以下代碼:
- Map<String, String> map = new HashMap<String, String>();
- 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 <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;
- }
虛擬機中沒有泛型,只有普通類和普通方法,所有泛型類的類型參數(shù)在編譯時都會被擦除,泛型類并沒有自己獨有的Class類對象。比如并不存在List
糖塊三、 自動裝箱與拆箱
自動裝箱就是Java自動將原始類型值轉(zhuǎn)換成對應(yīng)的對象,比如將int的變量轉(zhuǎn)換成Integer對象,這個過程叫做裝箱,反之將Integer對象轉(zhuǎn)換成int類型值,這個過程叫做拆箱。參考:一文讀懂什么是Java中的自動拆裝箱
因為這里的裝箱和拆箱是自動進行的非人為轉(zhuǎn)換,所以就稱作為自動裝箱和拆箱。
原始類型byte, short, char, int, long, float, double 和 boolean 對應(yīng)的封裝類為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();
- }
從反編譯得到內(nèi)容可以看出,在裝箱的時候自動調(diào)用的是Integer的valueOf(int)方法。而在拆箱的時候自動調(diào)用的是Integer的intValue方法。
所以,裝箱過程是通過調(diào)用包裝器的valueOf方法實現(xiàn)的,而拆箱過程是通過調(diào)用包裝器的 xxxValue方法實現(xiàn)的。
糖塊四 、 方法變長參數(shù)
可變參數(shù)(variable arguments)是在Java 1.5中引入的一個特性。它允許一個方法把任意數(shù)量的值作為參數(shù)。
看下以下可變參數(shù)代碼,其中print方法接收可變參數(shù):
- 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 < strs.length; i++)
- {
- System.out.println(strs[i]);
- }
- }
反編譯后代碼:
- public static void main(String args[])
- {
- print(new String[] {
- "Holis", "\u516C\u4F17\u53F7:Hollis", "\u53***\u5BA2\uFF1Awww.hollischuang.com", "QQ\uFF1A907607222"
- });
- }
- public static transient void print(String strs[])
- {
- for(int i = 0; i < strs.length; i++)
- System.out.println(strs[i]);
- }
從反編譯后代碼可以看出,可變參數(shù)在被使用的時候,他首先會創(chuàng)建一個數(shù)組,數(shù)組的長度就是調(diào)用該方法是傳遞的實參的個數(shù),然后再把參數(shù)值全部放到這個數(shù)組當(dāng)中,然后再把這個數(shù)組作為參數(shù)傳遞到被調(diào)用的方法中。
糖塊五 、 枚舉
Java SE5提供了一種新的類型-Java的枚舉類型,關(guān)鍵字enum可以將一組具名的值的有限集合創(chuàng)建為一種新的類型,而這些具名的值可以作為常規(guī)的程序組件使用,這是一種非常有用的功能。參考:Java的枚舉類型用法介紹
要想看源碼,首先得有一個類吧,那么枚舉類型到底是什么類呢?是enum嗎?
答案很明顯不是,enum就和class一樣,只是一個關(guān)鍵字,他并不是一個類。
那么枚舉是由什么類維護的呢,我們簡單的寫一個枚舉:
- public enum t {
- SPRING,SUMMER;
- }
然后我們使用反編譯,看看這段代碼到底是怎么實現(xiàn)的,反編譯后代碼內(nèi)容如下:
- 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關(guān)鍵字告訴我們,這個類也是不能被繼承的。
當(dāng)我們使用enmu來定義一個枚舉類型的時候,編譯器會自動幫我們創(chuàng)建一個final類型的類繼承Enum類,所以枚舉類型不能被繼承。
糖塊六 、 內(nèi)部類
內(nèi)部類又稱為嵌套類,可以把內(nèi)部類理解為外部類的一個普通成員。
內(nèi)部類之所以也是語法糖,是因為它僅僅是一個編譯時的概念。
outer.java里面定義了一個內(nèi)部類inner,一旦編譯成功,就會生成兩個完全不同的.class文件了,分別是outer.class和outer$inner.class。所以內(nèi)部類的名字完全可以和它的外部類名字相同。
- 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 。
當(dāng)我們嘗試使用jad對OutterClass.class文件進行反編譯的時候,命令行會打印以下內(nèi)容:
- Parsing OutterClass.class...
- Parsing inner class OutterClass$InnerClass.class...
- Generating OutterClass.jad
他會把兩個文件全部進行反編譯,然后一起生成一個OutterClass.jad文件。文件內(nèi)容如下:
- 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;
- }
糖塊七 、條件編譯
—般情況下,程序中的每一行代碼都要參加編譯。但有時候出于對程序代碼優(yōu)化的考慮,希望只對其中一部分內(nèi)容進行編譯,此時就需要在程序中加上條件,讓編譯器只對滿足條件的代碼進行編譯,將不滿足條件的代碼舍棄,這就是條件編譯。
如在C或CPP中,可以通過預(yù)處理語句來實現(xiàn)條件編譯。其實在Java中也可實現(xiàn)條件編譯。我們先來看一段代碼:
- 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;
- }
- }
首先,我們發(fā)現(xiàn),在反編譯后的代碼中沒有System.out.println("Hello, ONLINE!");,這其實就是條件編譯。
當(dāng)if(ONLINE)為false的時候,編譯器就沒有對其內(nèi)的代碼進行編譯。
所以,Java語法的條件編譯,是通過判斷條件為常量的if語句實現(xiàn)的。根據(jù)if判斷條件的真假,編譯器直接把分支為false的代碼塊消除。通過該方式實現(xiàn)的條件編譯,必須在方法體內(nèi)實現(xiàn),而無法在正整個Java類的結(jié)構(gòu)或者類的屬性上進行條件編譯。
這與C/C++的條件編譯相比,確實更有局限性。在Java語言設(shè)計之初并沒有引入條件編譯的功能,雖有局限,但是總比沒有更強。
糖塊八 、 斷言
在Java中,assert關(guān)鍵字是從JAVA SE 1.4 引入的,為了避免和老版本的Java代碼中使用了assert關(guān)鍵字導(dǎo)致錯誤,Java在執(zhí)行的時候默認是不啟動斷言檢查的(這個時候,所有的斷言語句都將忽略!)。
如果要開啟斷言檢查,則需要用開關(guān)-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("\u53***\u5BA2\uFF1Awww.hollischuang.com");
- return;
- }
- }
- static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desiredAssertionStatus();
- }
很明顯,反編譯之后的代碼要比我們自己的代碼復(fù)雜的多。所以,使用了assert這個語法糖我們節(jié)省了很多代碼。
其實斷言的底層實現(xiàn)就是if語言,如果斷言結(jié)果為true,則什么都不做,程序繼續(xù)執(zhí)行,如果斷言結(jié)果為false,則程序拋出AssertError來打斷程序的執(zhí)行。
-enableassertions會設(shè)置$assertionsDisabled字段的值。
糖塊九 、 數(shù)值字面量
在java 7中,數(shù)值字面量,不管是整數(shù)還是浮點數(shù),都允許在數(shù)字之間插入任意多個下劃線。這些下劃線不會對字面量的數(shù)值產(chǎn)生影響,目的就是方便閱讀。
比如:
- 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);
- }
- }
反編譯后就是把_刪除了。也就是說編譯器并不認識在數(shù)字字面量中的_,需要在編譯階段把他去掉。
糖塊十 、 for-each
增強for循環(huán)(for-each)相信大家都不陌生,日常開發(fā)經(jīng)常會用到的,他會比for循環(huán)要少寫很多代碼,那么這個語法糖背后是如何實現(xiàn)的呢?
- 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", "\u53***\u5BA2\uFF1Awww.hollischuang.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("Hollis", "\u516C\u4F17\u53F7\uFF1AHollis", "\u53***\u5BA2\uFF1Awww.hollischuang.com");
- String s;
- for(Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s))
- s = (String)iterator.next();
- }
代碼很簡單,for-each的實現(xiàn)原理其實就是使用了普通的for循環(huán)和迭代器。
糖塊十一 、 try-with-resource
Java里,對于文件操作IO流、數(shù)據(jù)庫連接等開銷非常昂貴的資源,用完之后必須及時通過close方法將其關(guān)閉,否則資源會一直處于打開狀態(tài),可能會導(dǎo)致內(nèi)存泄露等問題。
關(guān)閉資源的常用方式就是在finally塊里是釋放,即調(diào)用close方法。比如,我們經(jīng)常會寫這樣的代碼:
- 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提供了一種更好的方式關(guān)閉資源,使用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去關(guān)閉流,并不會使用在finally中寫很多代碼的方式,但是這種新的語法糖看上去好像優(yōu)雅很多呢。
反編譯以上代碼,看下他的背后原理:
- 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;
- }
- }
其實背后的原理也很簡單,那些我們沒有做的關(guān)閉資源的操作,編譯器都幫我們做了。
所以,再次印證了,語法糖的作用就是方便程序員的使用,但最終還是要轉(zhuǎn)成編譯器認識的語言。
糖塊十二、Lambda表達式
關(guān)于lambda表達式,有人可能會有質(zhì)疑,因為網(wǎng)上有人說他并不是語法糖。其實我想糾正下這個說法。
Labmda表達式不是匿名內(nèi)部類的語法糖,但是他也是一個語法糖。實現(xiàn)方式其實是依賴了幾個JVM底層提供的lambda相關(guān)api。
先來看一個簡單的lambda表達式。遍歷一個list:
- public static void main(String... args) {
- List<String> strList = ImmutableList.of("Hollis", "公眾號:Hollis", "博客:www.hollischuang.com");
- strList.forEach( s -> { System.out.println(s); } );
- }
為啥說他并不是內(nèi)部類的語法糖呢,前面講內(nèi)部類我們說過,內(nèi)部類在編譯之后會有兩個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(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方法中,其實是調(diào)用了java.lang.invoke.LambdaMetafactory#metafactory方法,該方法的第四個參數(shù)implMethod指定了方法實現(xiàn)??梢钥吹竭@里其實是調(diào)用了一個lambda$main$0方法進行了輸出。
再來看一個稍微復(fù)雜一點的,先對List進行過濾,然后再輸出:
- public static void main(String... args) {
- List<String> 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(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表達式分別調(diào)用了lambda$main$1和lambda$main$0兩個方法。
所以,lambda表達式的實現(xiàn)其實是依賴了一些底層的api,在編譯階段,編譯器會把lambda表達式進行解糖,轉(zhuǎn)換成調(diào)用內(nèi)部api的方式。
可能遇到的坑
泛型——當(dāng)泛型遇到重載
- 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)");
- }
- }
上面這段代碼,有兩個重載的函數(shù),因為他們的參數(shù)類型不同,一個是List另一個是List,但是,這段代碼是編譯通不過的。因為我們前面講過,參數(shù)List和List編譯之后都被擦除了,變成了一樣的原生類型List,擦除動作導(dǎo)致這兩個方法的特征簽名變得一模一樣。
泛型——當(dāng)泛型遇到catch
泛型的類型參數(shù)不能用在Java異常處理的catch語句中。因為異常處理是由JVM在運行時刻來進行的。由于類型信息被擦除,JVM是無法區(qū)分兩個異常類型MyException
泛型——當(dāng)泛型內(nèi)包含靜態(tài)變量
- 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){}
- }
以上代碼輸出結(jié)果為:2!由于經(jīng)過類型擦除,所有的泛型類實例都關(guān)聯(lián)到同一份字節(jié)碼上,泛型類的所有靜態(tài)變量是共享的。
自動裝箱與拆箱——對象相等比較
- 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)));
- }
輸出結(jié)果:
- a == b is false
- c == d is true
在Java 5中,在Integer的操作上引入了一個新功能來節(jié)省內(nèi)存和提高性能。整型對象通過使用相同的對象引用實現(xiàn)了緩存和重用。
適用于整數(shù)值區(qū)間-128 至 +127。
只適用于自動裝箱。使用構(gòu)造函數(shù)創(chuàng)建對象不適用。
增強for循環(huán)
- for (Student stu : students) {
- if (stu.getId() == 2)
- students.remove(stu);
- }
會拋出ConcurrentModificationException異常。
Iterator是工作在一個獨立的線程中,并且擁有一個 mutex 鎖。 Iterator被創(chuàng)建之后會建立一個指向原來對象的單鏈索引表,當(dāng)原來的對象數(shù)量發(fā)生變化時,這個索引表的內(nèi)容不會同步改變,所以當(dāng)索引指針往后移動的時候就找不到要迭代的對象,所以按照 fail-fast 原則 Iterator 會馬上拋出java.util.ConcurrentModificationException異常。參考:一不小心就讓Java開發(fā)者踩坑的fail-fast是個什么鬼?
所以 Iterator 在工作的時候是不允許被迭代的對象被改變的。但你可以使用 Iterator 本身的方法remove()來刪除對象,Iterator.remove() 方法會在刪除當(dāng)前迭代對象的同時維護索引的一致性。
總結(jié)
前面介紹了12種Java中常用的語法糖。所謂語法糖就是提供給開發(fā)人員便于開發(fā)的一種語法而已。
但是這種語法只有開發(fā)人員認識。要想被執(zhí)行,需要進行解糖,即轉(zhuǎn)成JVM認識的語法。
當(dāng)我們把語法糖解糖之后,你就會發(fā)現(xiàn)其實我們?nèi)粘J褂玫倪@些方便的語法,其實都是一些其他更簡單的語法構(gòu)成的。
有了這些語法糖,我們在日常開發(fā)的時候可以大大提升效率,但是同時也要避免過渡使用。使用之前***了解下原理,避免掉坑。