目錄
面試回答
知識擴展
如何解語法糖?
糖塊一、swith 支持 String 與枚舉
糖塊二、泛型
糖塊三、自動裝箱與拆箱
糖塊四、枚舉
糖塊五、條件編譯
糖塊六、斷言
糖塊七、數值字面量
糖塊八、for-each
糖塊九、try-with-resource
可能遇到的坑
泛型
自動裝箱與拆箱
總結
面試回答
語法糖(Syntactic sugar),指在計算機語言中添加的某種語法,這種語法對語言的功能并沒有影響,但是更方便程序員使用。
雖然 Java 中有很多語法糖,但是 Java 虛擬機并不支持這些語法糖,所以這些語法糖在編譯階段就會被還原成簡單的基礎語法結構,這樣才能被虛擬機識別,這個過程就是解語法糖。
如果看過 Java 虛擬機的源碼,就會發現在編譯過程中有一個重要的步驟就是調用 desugar() ,這個方法就是負責解語法糖的實現。
常見的語法糖有 switch 支持枚舉及字符串、泛型、條件編譯、斷言、可變參數、自動裝箱/拆箱、枚舉、內部類、增強 for 循環、try-with-resources 語句、lambda 表達式等。
知識擴展
如何解語法糖?
語法糖的存在主要是方便開發人員使用。但其實,Java 虛擬機并不支持這些語法糖。這些語法糖在編譯階段就會被還原成簡單的基礎語法結構,這個過程就是解語法糖。
說到編譯,大家肯定都知道,Java 語言中 javac 命令可以將后綴名為 .java
的源文件編譯為 .class
的可以運行于 java 虛擬機的字節碼。如果你去看 com.sun.tools.javac.main.JavaCompiler
的源碼,你會發現在 compile()
中有一個步驟就是調用 desugar()
,這個方法就是負責解語法糖的實現的。
糖塊一、swith 支持 String 與枚舉
從 Java 7 開始,Java 語言中的語法糖在逐漸豐富,其中一個比較重要的就是 Java 7 中 swith
開始支持 String
。
在開始 coding 之前先科普下,Java 中的 switch
自身原本就支持基本類型。比如 int
、char
等。對于 int
類型,直接進行數值的比較。對于 char
類型則是比較其 ascii 碼。所以,對于編譯器來說,switch 中其實只能使用整型,任何類型的比較都要轉換成整型。比如 byte
、short
、char
以及int
(ascii 碼是整型)。
那么接下來看下 switch
對 String
的支持,如以下代碼:
public class main {public static void main(String[] args) {String str="word";switch (str){case "hello":System.out.println("hello");break;case "world":System.out.println("world");break;default:break;}}
}
反編譯后內容如下:
public class main
{public main(){}public static void main(String args[]){String str = "word";String s = str;byte byte0 = -1;switch(s.hashCode()){case 99162322: if(s.equals("hello"))byte0 = 0;break;case 113318802: if(s.equals("world"))byte0 = 1;break;}switch(byte0){case 0: // '\0'System.out.println("hello");break;case 1: // '\001'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","tango");map.put("wechat","Tango");map.put("blog","https://www.baidu.com");
解語法糖之后會變成:
Map map = new HashMap();map.put("name", "tango");map.put("wechat", "Tango");map.put("blog", "https://www.baidu.com");
以下代碼:
public static <A extends Comparable<A>> A max(Collection<A> xs) {Iterator<A> xi = xs.iterator();A w = (Comparable)xi.next();while(xi.hasNext()) {A x = (Comparable)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();do{if(!xi.hasNext())break;Comparable x = (Comparable)xi.next();if(w.compareTo(x) < 0)w = x;} while(true);return w;}
虛擬機中沒有泛型,只有普通類和普通方法,所欲泛型類的類型參數在編譯時都會被擦除,泛型類并沒有自己獨有的 Class
類對象。比如并不存在 List<String>.class 或是 List<Integer>.class ,而只有 List.class 。
糖塊三、自動裝箱與拆箱
自動裝箱就是 Java 自動將原始類型值轉換成對應的對象,比如將 int 的變量轉換成 Integer 的對象,這個過程叫做裝箱,反之將 Integer 對象轉換成 int 類型值,這個過程叫做拆箱。因為這里的裝箱和拆箱是自動進行的非人為轉換,所以就稱作為自動裝箱和拆箱。原始類型 byte、short、char、int、long、float、double 和 boolan 對應的封裝類為 Byte、Short、Character、Integer、Long、Float、Double、Boolean。
先來看個自動裝箱的代碼:
public static void main(String[] args) {int i = 0;Integer n = i;}
反編譯后代碼如下:
public static void main(String args[]){int i = 0;Integer n = Integer.valueOf(i);}
再來看個自動拆箱的代碼:
public static void main(String[] args) {Integer n = 0;int i = n;}
反編譯后代碼如下:
public static void main(String args[]){Integer n = Integer.valueOf(0);int i = n.intValue();}
從反編譯得到內容可以看出,在裝箱的時候自動調用的是 Integer
的 valueOf
方法。而在拆箱的時候自動調用的是 Integer
的 intValue
的方法。
所以,裝箱過程是通過調用包裝器的 valueOf 方法實現的,而拆箱過程是通過調用包裝器的 xxxValue 方法實現的。
糖塊四、枚舉
在 Java 中,枚舉是一種特殊的數據類型,用于表示有限的一組常量。枚舉常量是在枚舉類型中定義的,每個常量都是該類型的一個實例。Java 中的枚舉類型是一種安全而優雅的方式來表示有限的一組值。
要想看源碼,首先得有一個類吧,那么枚舉類型到底是什么類呢?是 enum
嗎?答案很明顯不是,enum
就和 class
一樣,只是一個關鍵字,他并不是一個類,那么枚舉是由什么類維護的呢?我們簡單的寫一個枚舉:
public enum t {SPRING,SUMMER;
}
然后我們使用反編譯,看看這段代碼到底是怎么實現的,反編譯后代碼如下:
public final class t extends Enum
{public static t[] values(){return (t[])$VALUES.clone();}public static t valueOf(String name){return (t)Enum.valueOf(com/chiyi/test/t, name);}private t(String s, int i){super(s, i);}public static final t SPRING;public static final t SUMMER;private static final t $VALUES[];static {SPRING = new t("SPRING", 0);SUMMER = new t("SUMMER", 1);$VALUES = (new t[] {SPRING, SUMMER});}
}
通過反編譯代碼我們可以看到,public final class t extends Enum
,說明,該類是繼承了 Enum
類的,同時 final
關鍵字告訴我們,這個類也是不能被繼承的。當我們使用 enum 來定義一個枚舉類型的時候,編譯器會自動幫我們創建一個 final
類型的類繼承 Enum
類,所以枚舉類型不能被繼承。
糖塊五、條件編譯
一般情況下,程序中的每一行代碼都要參加編譯。但有時候出于對程序代碼優化的考慮,希望只對其中一部分內容進行編譯,此時就需要在程序中加上條件,讓編譯器只對滿足條件的代碼進行編譯,將不滿足條件的代碼舍棄,這就是條件編譯。
如在 C 或 CPP 中,可以通過預處理語句來實現條件編譯。其實在 Java 中也可實現條件編譯。我們先來看一段代碼:
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 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 語句實現的。其原理也是 Java 語言的語法糖。根據 if 判斷條件的真假,編譯器直接把分支為 false 的代碼塊消除。通過該方式實現的條件編譯,必須在方法體內實現,而無法在整個 Java 類的結構或類的屬性上進行條件編譯,這與 C/C++ 的條件編譯相比,確實更有局限性。在 Java 語言設計之初并沒有引入條件編譯的功能。雖有局限,但是總比沒有更強、
糖塊六、斷言
在 Java 中, assert
關鍵字是從 JAVA SE 1.4 引入的,為了避免和老版本的 Java 代碼中使用了 assert 關鍵字導致錯誤, Java 執行的時候默認是不啟動斷言檢查的(這個時候,所有的斷言語句都將忽略!),如果要開啟斷言檢查,則需要用開關 -enableassertions
或 -ea
來開啟。
看一段包含斷言的代碼:
public static void main(String[] args) {int a = 1;int b = 1;assert a == b;System.out.println("公眾號:Tango");assert a != b : "Tango";System.out.println("百度:https://www.baidu.com");}
反編譯后代碼如下:
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\uFF1ATango");if(!$assertionsDisabled && a == b){throw new AssertionError("Tango");} else{System.out.println("\u767E\u5EA6\uFF1Ahttps://www.baidu.com");return;}}
很明顯,反編譯之后的代碼要比我們自己的代碼復雜得多。所以,使用了 assert 這個語法糖我們節省了很多代碼。其實斷言的底層實現就是 if 語句,如果斷言結果為 true,則什么都不做,程序繼續執行,如果斷言結果為 false,則程序拋出 AssertError 來打斷程序的執行。-enableassertions
會設置 $assertionsDisabled 字段的值。
糖塊七、數值字面量
在 java 7 中,數值字面量,不管是整數還是浮點數,都允許在數字之間插入任意多個下劃線。這些下劃線不會對字面量的數值產生影響,目的就是方便閱讀。
比如:
public static void main(String[] args) {int i=10_000;System.out.println(i);}
反編譯后:
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={"南京","合肥","深圳","北京"};for (String s:strs){System.out.println(s);}List<String> strings= ImmutableList.of("南京","合肥","深圳","北京") ;for (String s:strings){System.out.println(s);}}
反編譯后代碼如下:
public static void main(String args[]){String strs[] = {"\u5357\u4EAC", "\u5408\u80A5", "\u6DF1\u5733", "\u5317\u4EAC"};String args1[] = strs;int i = args1.length;for(int j = 0; j < i; j++){String s = args1[j];System.out.println(s);}List strings = ImmutableList.of("\u5357\u4EAC", "\u5408\u80A5", "\u6DF1\u5733", "\u5317\u4EAC");String s;for(Iterator iterator = strings.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:\\youth\\java\\javacode\\base\\src\\main\\resources\\application.properties"));while ((line = br.readLine()) != null) {System.out.println(line);}} catch (IOException e) {} 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:\\tango.xml"))){String line;while ((line = br.readLine()) != null) {System.out.println(line);}} catch (IOException e) {//handle exception}}
看,這簡直是一大福音啊,雖然我之前一般使用 IOUtils
去關閉流,并不會使用在 finally
中寫很多代碼的方式,但是這種新的語法糖看上去好像優雅很多呢。看下他的背后:
public static void main(String args[]){BufferedReader br;Throwable throwable;br = new BufferedReader(new FileReader("D:\\tango.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);}elsebr.close();break MISSING_BLOCK_LABEL_113;Exception exception;exception;if(br != null)if(throwable != null)try{br.close();}catch(Throwable throwable3){throwable.addSuppressed(throwable3);}elsebr.close();throw exception;IOException ioexception;ioexception;}
其實背后的原理也很簡單,那些我們沒有做的關閉資源的操作,編譯器都幫我們做了。所以,再次印證了,語法糖的作用就是方便程序員的作用,但最終還是要轉成編譯器認識的語言。
可能遇到的坑
泛型
一、當泛型遇到重載
比如:
public static void print(List<String> list){System.out.println("invoke print(List<String> list)");}public static void print(List<Integer> list){System.out.println("invoke print(List<String> list)");}
上面這段代碼,有兩個重載的函數,因為他們的參數類型不同,一個是 List<String>,另一個是 List<Integer>,但是,這段代碼是編譯通不過的。因為我們前面講過,參數 List 和 List 編譯之后都被擦除了,變成了一樣的原生類型 List,擦除動作導致這兩個方法的特征簽名變得一模一樣。
二、當泛型遇到 catch 泛型的類型參數不能用在 Java 異常處理的 catch 語句中。因為異常處理是由 JVM 在運行時刻來進行的。由于類型信息被擦除,JVM 是無法區分兩個異常類型 MyException<String>
和 MyException<Integer>
的。
三、當泛型內含靜態變量
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);}static 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。
只適用于自動裝箱。使用構造函數創建對象不適用。
總結
前面介紹了 9 種 Java 中常用的語法糖。所謂語法糖就是提供給開發人員便于開發的一種語法而已。但是這種語法只有開發人員認識。想要被執行,需要進行解糖,即轉成 JVM 認識的語法。當我們把語法糖解糖之后,你就會發現其實我們日常使用的這些方便的語法,其實都是一些其他更簡單的語法構成的。
有了這些語法糖,我們在日常開發的時候可以大大提升效率,但是同時也要避免過度使用。使用之前最好了解下原理,避免掉坑。