Java泛型的實現原理
泛型,就是一個參數化了的類或介面。裸類型(raw type)就是指去掉了型別參數資訊的類型。Java 為了保持相容性,泛型的實現並不像在C#中那麼徹底,看下面一個具體的泛型類,
public class Node<T, U extends Number> {private T data;private List<U> list;private Node<T> next;public Node(T data, Node<T> next) {this.data = data;this.next = next;} public void setData2(List<U> l) {list = l; }public U getFirstData(){return list.get(0); }public T getData() { return data; }// ... }
在編譯這個泛型的時候,編譯器會做一個叫做去泛型型別(Erasure of Generic Types)的處理,具體的處理內容如下:
[1] 泛型型別參數被直接去掉,並把所有的型別參數進行替換。對有Bounded的參數,則使用相應的邊界類型,例如,如果泛型參數是<U extends Number>,那麼這個參數會被直接轉換為Number。如果是類型是像我們上面的這種<T>,那麼<T>會被裝換為Object。
[2] 進行必要的類型裝換,以保證型別安全。
[3] 產生橋接方法保證整合泛型型別時,多態特性仍然工作正常。
去泛型型別(Erasure of Generic Types)和相關的強制類型轉換
這個主要和[1]、[2] 兩條相關,根據第[1]條,Java編譯器會把這個泛型類編譯為如下:
//型別參數被直接去掉 public class Node { //型別參數T被替換為Object private Object data; //U被替換為Number private List data2; //型別參數被直接去掉 private Node next; //型別參數被直接去掉,型別參數T被替換為Object public Node(Object data, Node next) { this.data = data; this.next = next; } //U被替換為Number public void setData2(List l) { list = l; } //U被替換為Number, 經過必要的類型轉換後,實際會變為 public Number getData2() { return (Number)list.get(0); } public Number getData2() { return list.get(0); } //型別參數T被替換為Object public Object getData() { return data; } // ... }
經過這個處理後,我們看看第23行經過處理後的代碼,這裡其實是有問題的。因為list.get(0)返回的是一個Object對象,而getData2方法的傳回值是一個Number類型,Object類型不能直接賦值給Number類型,所以這裡必須做一個強制裝換。這也是我們上面說到的第[2] 條的意義所在,經過第[2] 條的規則,第23行實際上會被編譯為:
public Number getData2() { return (Number)list.get(0); }
這樣,不管我們在使用泛型
的時候使用什麼具體的類型,上面的代碼都是能夠保證型別安全的。例如,
Node<String, Integer> node1 = new Node<String, Integer>(); Node<List, Short> node2 = new Node<List, Short>();
在使用的時候,Node<String, Integer>和Node<List, Short>都被轉為了Node進行使用,並沒有Node<String, Integer>和Node<List, Short>這兩種類型的存在。編譯器會進行類型轉換以保證在調用相關方法時的型別安全,這需要做強制類型轉換,比如我們寫如下的代碼:
Node<String, Integer> node1 = new Node<String, Integer>(); Integer somenumber = node1.getData2()
這段代碼會被編譯為類似如下,因為getData2返回的是一個Number類型,這裡必須做一個強制轉換:
Node<String, Integer> node1 = new Node<String, Integer>(); Integer somenumber = (Integer)node1.getData2();
橋接方法(Bridge Methods)
先看一個例子如下(例子照搬Oracle的相關文檔):
public class Node<T> { private T data; public Node(T data) { this.data = data; } public void setData(T data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node<Integer> { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } }
更具我們前面已經講到的,這段代碼在經過去泛型型別(Erasure of Generic Types)後,會變為如下的樣子:
public class Node { private Object data; public Node(Object data) { this.data = data; } public void setData(Object data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println(Integer data); super.setData(data); } }
注意看這裡,經過去泛型型別(Erasure of Generic Types)後,考慮如下的代碼:
MyNode mn = new MyNode(5);Node n = (MyNode)mn; n.setData("Hello"); mn.data //??
這裡特別注意第3行,因為基類和子類分別有一個setData(Object)和setData(Integer)方法,由於方法的參數不同,這兩個實際上是重載方法,所以第三行回去調用Node的setData(Object)。所以,這個時候data到底是個什麼值?明顯這裡是有問題的,繼承的多態性沒有儲存下來。這裡就需要做我們之前提到的第[3]條了,編譯器會給子類添加一個setData(Object)方法,這個方法稱為橋接方法如下:
class MyNode extends Node { //編譯器自動產生的橋接方法 public void setData(Object data) {setData((Integer) data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } // ... }
現在再看下面的代碼:
MyNode mn = new MyNode(5);Node n = (MyNode)mn;n.setData("Hello");
這裡的setData會去調用基類的方法,當然,這裡運行時是會出錯的,因為無法將字串轉為整型。這裡目的就是保持泛型繼承的多態性。