final
// 方法一帶有final修飾
public void foo(final int arg) {
final int var = 0;
// do something
}
// 方法二沒有final修飾
public void foo(int arg) {
int var = 0;
// do something
}
觀察這兩段代碼編譯出來的字節碼,會發現它們是沒有任何一點區別的,每條指令,甚至每個字節都一模一樣。可以肯定地推斷出把局部變量聲明為final,對運行期是完全沒有影響的,變量的不變性僅僅由Javac編譯器在編譯期間來保障,這就是一個只能在編譯期而不能在運行期中檢查的例子。
語法糖
指的是在計算機語言中添加的某種語法,這種語法對語言的編譯結果和功能并沒有實際影響,但是卻能更方便程序員使用該語言。通常來說使用語法糖能夠減少代碼量、增加程序的可讀性,從而減少程序代碼出錯的機會。
Java中最常見的語法糖包括了前面提到過的泛型、變長參數、自動裝箱拆箱,等等,Java虛擬機運行時并不直接支持這些語法,它們在編譯階段被還原回原始的基礎語法結構,這個過程就稱為解語法糖。
泛型
在2004年,Java和C#兩門語言于同一年更新了一個重要的大版本,即Java 5.0和C#2.0,在這個大版本中,兩門語言又不約而同地各自添加了泛型的語法特性。本來Java和C#天生就存在著比較和競爭,泛型這個兩門語言在同一年、同一個功能上做出的不同選擇,自然免不了被大家對比審視一番,其結論是Java的泛型直到今天依然作為Java語言不如C#語言好用的“鐵證”被眾人嘲諷。
Java選擇的泛型實現方式叫作“類型擦除式泛型”(Type Erasure Generics),而C#選擇的泛型實現方式是“具現化式泛型”(Reified Generics)。
Java的泛型確實在實際使用中會有一些限制,如果讀者是一名C#開發人員,可能很難想象以下Java代碼都是不合法的。
public class TypeErasureGenerics<E> {
public void doSomething(Object item) {
if (item instanceof E) { // 不合法,無法對泛型進行實例判斷
...
}
E newItem = new E(); // 不合法,無法使用泛型創建對象
E[] itemArray = new E[10]; // 不合法,無法使用泛型創建數組
}
}
上面這些是Java泛型在編碼階段產生的不良影響,如果說這種使用層次上的差別還可以通過多寫幾行代碼、方法中多加一兩個類型參數來解決的話,性能上的差距則是難以用編碼彌補的。C#2.0引入了泛型之后,帶來的顯著優勢之一便是對比起Java在執行性能上的提高,因為在使用平臺提供的容器類型(如List<T>,Dictionary<TKey,TValue>)時,無須像Java里那樣不厭其煩地拆箱和裝箱,如果在Java中要避免這種損失,就必須構造一個與數據類型相關的容器類(譬如IntFloatHashMap這樣的容器)。顯然,這除了引入更多代碼造成復雜度提高、復用性降低之外,更是喪失了泛型本身的存在價值。
Java的類型擦除式泛型無論在使用效果上還是運行效率上,幾乎是全面落后于C#的具現化式泛型,而它的唯一優勢是在于實現這種泛型的影響范圍上:擦除式泛型的實現幾乎只需要在Javac編譯器上做出改進即可,不需要改動字節碼、不需要改動Java虛擬機,也保證了以前沒有使用泛型的庫可以直接運行在Java 5.0之上。
這是由于《Java語言規范》中的對Java使用者的嚴肅承諾,譬如一個在JDK 1.2中編譯出來的Class文件,必須保證能夠在JDK 12乃至以后的版本中也能夠正常運行。這樣,既然Java到1.4.2版之前都沒有支持過泛型,而到Java 5.0突然要支持泛型了,還要讓以前編譯的程序在新版本的虛擬機還能正常運行,就意味著以前沒有的限制不能突然間冒出來。
設計者面前大體上有兩條路可以選擇:
1)需要泛型化的類型(主要是容器類型),以前有的就保持不變,然后平行地加一套泛型化版本
的新類型。
2)直接把已有的類型泛型化,即讓所有需要泛型化的已有類型都原地泛型化,不添加任何平行于已有類型的泛型版。
在這個分叉路口,C#走了第一條路,添加了一組System.Collections.Generic的新容器,以前的System.Collections以及System.Collections.Specialized容器類型繼續存在。C#的開發人員很快就接受了新的容器,倒也沒出現過什么不適應的問題,唯一的不適大概是許多.NET自身的標準庫已經把老容器類型當作方法的返回值或者參數使用,這些方法至今還保持著原來的老樣子。
但如果相同的選擇出現在Java中就很可能不會是相同的結果了,要知道當時.NET才問世兩年,而Java已經有快十年的歷史了,再加上各自流行程度的不同,兩者遺留代碼的規模根本不在同一個數量級上。而且更大的問題是Java并不是沒有做過第一條路那樣的技術決策,在JDK 1.2時,遺留代碼規模尚小,Java就引入過新的集合類,并且保留了舊集合類不動。這導致了直到現在標準類庫中還有Vector(老)和ArrayList(新)、有Hashtable(老)和HashMap(新)等兩套容器代碼并存,如果當時再擺弄出像Vector(老)、ArrayList(新)、Vector<T>(老但有泛型)、ArrayList<T>(新且有泛型)這樣的容器集合,可能叫罵聲會比今天聽到的更響更大。
泛型擦除前的例子
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("hello", "你好");
map.put("how are you?", "吃了沒?");
System.out.println(map.get("hello"));
System.out.println(map.get("how are you?"));
}
把這段Java代碼編譯成Class文件,然后再用字節碼反編譯工具進行反編譯后,將會發現泛型都不見了,程序又變回了Java泛型出現之前的寫法,泛型類型都變回了裸類型,只在元素訪問時插入了從Object到String的強制轉型代碼
泛型擦除后的例子
public static void main(String[] args) {
Map map = new HashMap();
map.put("hello", "你好");
map.put("how are you?", "吃了沒?");
System.out.println((String) map.get("hello"));
System.out.println((String) map.get("how are you?"));
}
問題:
1,使用擦除法實現泛型直接導致了對原始類型(Primitive Types)數據的支持又成了新的麻煩。因為不支持int、long與Object之間的強制轉型。當時Java給出的解決方案一如既往的簡單粗暴:既然沒法轉換那就索性別支持原生類型的泛型了吧,你們都用ArrayList<Integer>、ArrayList<Long>,反正都做了自動的強制類型轉換,遇到原生類型時把裝箱、拆箱也自動做了得了。這個決定后面導致了無數構造包裝類和裝箱、拆箱的開銷,成為Java泛型慢的重要原因,也成為今天Valhalla項目要重點解決的問題之一。
2,運行期無法取到泛型類型信息,我們去寫一個泛型版本的從List到數組的轉換方法,由于不能從List中取得參數化類型T,所以不得不從一個額外參數中再傳入一個數組的組件類型進去,實屬無奈。
public static <T> T[] convert(List<T> list, Class<T> componentType) {
T[] array = (T[])Array.newInstance(componentType, list.size());
...
}
3,擦除法來實現泛型,還喪失了一些面向對象思想應有的優雅,帶來了一些模棱兩可的模糊狀況
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<Integer>和List<String>編譯之后都被擦除了,變成了同一種的裸類型List,類型擦除導致這兩個方法的特征簽名變得一模一樣。
為了彌補擦除的不足,新增了Signature屬性。Signature屬性的作用是存儲一個方法在字節碼層面的特征簽名,這個屬性保存的參數類型不是原生類型,而是包括了參數化類型的信息。
泛型與類型擦除;Java泛型-4(類型擦除后如何獲取泛型參數)
摘抄:《深入理解Java虛擬機:JVM高級特性與最佳實踐》-第十章