總結Java的泛型前,先簡單的介紹下C#的泛型,通過對比,比較學習Java泛型的目的和設計意圖。C#泛型是C#語言2.0和通用語言運行時(CLR)同時支持的一個特性(這一點是導致C#泛型和Java泛型區別的最大原因,后面會介紹)。C#泛型在.NET CLR支持為.NET框架引入參數化變量支持。C#泛型更類似C++模板,可以理解,C#泛型實際上可以理解為類的模板類。我們通過代碼實例來看C# 2.0泛型解決的問題,首先,我們通過一個沒有泛型的迭代器的代碼示例說起,代碼實現如下:
interface IEnumerable { void Add(Object x); IEnumerator GetEnumerator(); } interface IEnumerator { Object Next(); bool HasNext(); } class NoSuchElementException : ArgumentException { } class LinkedList : IEnumerable { private class Node { public Object Data { get; set; } public Node Next { get; set; } public Node(Object data) { this.Data = data; } } class DataEnumerator : IEnumerator { public DataEnumerator(Node ptr) { this.head = ptr; } private Node head; public object Next() { if (head != null) { Object data = head.Data; head = head.Next; return data; } else { throw new NoSuchElementException(); } } public bool HasNext() { return head != null; } } private Node head = null, tail = null; public void Add(Object data) { if (head == null) { head = new Node(data); tail = head; } else { tail.Next = new Node(data); tail = tail.Next; } } public IEnumerator GetEnumerator() { return new DataEnumerator(head); } }
通過以上C#代碼對迭代器的實現,我們發現代碼至少存在以下幾個問題:
- 如果我不想在對LinkedList集合對象存儲的時候發生裝箱拆箱的性能損失,只是對特點類型的元素集合進行操作。如:我們LinkedList存儲了100w的整數,用LinkedList存儲的時候,每次都把整數 轉出 Object,相當於在堆內存中對數字進行了100w次打包,其中的性能損失我們不能不考慮。我們考慮性能的話,我們可以修改LinkedList的實現,將內嵌類Node的Data數據類型改為int類型,修改后的實現如下:
interface IEnumerable { void Add(int x); IEnumerator GetEnumerator(); } interface IEnumerator { int Next(); bool HasNext(); } class NoSuchElementException : ArgumentException { } class LinkedList : IEnumerable { private class Node { public int Data { get; set; } public Node Next { get; set; } public Node(int data) { this.Data = data; } } class DataEnumerator : IEnumerator { public DataEnumerator(Node ptr) { this.head = ptr; } private Node head; public int Next() { if (head != null) { int data = head.Data; head = head.Next; return data; } else { throw new NoSuchElementException(); } } public bool HasNext() { return head != null; } } private Node head = null, tail = null; public void Add(int data) { if (head == null) { head = new Node(data); tail = head; } else { tail.Next = new Node(data); tail = tail.Next; } } public IEnumerator GetEnumerator() { return new DataEnumerator(head); } }
- LinkedList linkedList = new LinkedList(),linkedList集合存儲的都是Object類型的元素,對集合進行操作的時候,如果對集合存儲的代碼調用時元素類型信息無法直接確定,無法判斷集合存儲的具體類型。例如,我們有如下代碼的調用:
class Program { static void Main(string[] args) { LinkedList list = new LinkedList(); list.Add(new A()); list.Add(new B()); IEnumerator e = list.GetEnumerator(); while (e.HasNext()) { B b = e.Next() as B; //此處類型轉換,當轉換不成功時候,捕獲異常,返回類型為空 if(b != null) b.MethodB(); } } } class A { public void MethodA() { Console.WriteLine("Invoke MethodA"); } } class B:A { public void MethodB() { Console.WriteLine("Invoke MethodA"); } }
以上代碼在調用 B b = e.Next() as B 語句的時候,此處類型轉換,當轉換不成功時候,捕獲異常,返回類型為空。
- LinkedList集合對象在存儲元素的時候,對元素進行隱式類型轉換(子類轉換成Object類型)或者(值類型發生裝箱操作)。
下面我們通過C#2.0開始支持的泛型來解決了以上的問題,泛型化修改后的代碼如下:
interface IEnumerable<T> { void Add(T x); IEnumerator<T> GetEnumerator(); } interface IEnumerator<T> { T Next(); bool HasNext(); } class NoSuchElementException<T> : ArgumentException { } class LinkedList<T> : IEnumerable<T> { private class Node { public T Data { get; private set; } public Node Next { get; set; } public Node(T data) { this.Data = data; } } class DataEnumerator : IEnumerator<T> { public DataEnumerator(Node ptr) { this.head = ptr; } private Node head; public T Next() { if (head != null) { T data = head.Data; head = head.Next; return data; } else { throw new NoSuchElementException<T>(); } } public bool HasNext() { return head != null; } } private Node head = null, tail = null; public void Add(T data) { if (head == null) { head = new Node(data); tail = head; } else { tail.Next = new Node(data); tail = tail.Next; } } public IEnumerator<T> GetEnumerator() { return new DataEnumerator(head); } }
通過以上對C#泛型分析,簡單總結下C#引入,在我看來至少解決了三個問題:
- 封裝和復用通用代碼邏輯;
- 增強了編譯期類型檢查,減少了運行時發生InvalidCastException異常的幾率;
- 解決集合操作時候的裝箱和拆箱的效率問題。
簡答介紹了C#泛型引入解決的問題后,下面我們開始介紹今天的主角—Java泛型。我們會對比以上三個問題來對比分析Java引入泛型所解決的問題。下面同樣來一段Java實現的迭代器的實現的代碼,如下:
interface Collection { public void add (Object x); public Iterator iterator (); } interface Iterator { public Object next (); public boolean hasNext (); } class NoSuchElementException extends RuntimeException {} class LinkedList implements Collection { protected class Node { Object elt; Node next = null; Node (Object elt) { this.elt = elt; } } protected Node head = null, tail = null; public LinkedList () {} public void add (Object elt) { if (head == null) { head = new Node(elt); tail = head; } else { tail.next = new Node(elt); tail = tail.next; } } public Iterator iterator () { return new Iterator () { protected Node ptr = head; public boolean hasNext () { return ptr != null; } public Object next () { if (ptr != null) { Object elt = ptr.elt; ptr = ptr.next; return elt; } else throw new NoSuchElementException (); } }; } }
對比C#和Java的迭代器實現,不得不說Java的迭代器實現看起來更優雅。Java對內部類的支持,讓Java在類型實現上更靈活多樣。下面我們來看看Java泛型迭代器的實現如下:
interface Collection<A> { public void add(A x); public Iterator<A> iterator(); } interface Iterator<A> { public A next(); public boolean hasNext(); } class NoSuchElementException extends RuntimeException {} class LinkedList<A> implements Collection<A> { protected class Node { A elt; Node next = null; Node (A elt) { this.elt = elt; } } protected Node head = null, tail = null; public LinkedList () {} public void add (A elt) { if (head == null) { head = new Node(elt); tail = head; } else { tail.next = new Node(elt); tail = tail.next; } } public Iterator<A> iterator () { return new Iterator<A> () { protected Node ptr = head; public boolean hasNext () { return ptr != null; } public A next () { if (ptr != null) { A elt = ptr.elt; ptr = ptr.next; return elt; } else throw new NoSuchElementException (); } }; } }
通過以上的代碼的實現,我們可能覺得Java泛型和C#都能很好解決以上三個問題。看過下面的一段代碼之后,可能你對Java泛型的會有不同的認識,代碼如下:
public class Program { public static void main(String[] args) { Class<?> c1 = new ArrayList<String>().getClass(); Class<?> c2 = new ArrayList<Integer>().getClass(); if(c1 == c2) { System.out.println("類型相同"); } } } //類型相同
按C#對泛型的理解,泛型實質就是類的模板。我們認為很容易知道類ArrayList<String>和ArrayList<Integer>應該是不同的類型。但是上面的代碼輸出結果,Java運行結果判斷ArrayList<String>和ArrayList<Integer>是相同的類型。
我們馬上會有疑問,在運行狀態,我們的泛型參數類型信息哪去了?我們的泛型怎么了?對java泛型有一定了解的同學馬上就知道,在運行狀態,我們無法獲取泛型的參數信息。Java的泛型不是真正的泛型,只是編譯器的泛型,不是運行時的泛型。關於為什么會這樣設計,我們就需要去追溯java的歷史。
Java泛型歷史
Java泛型是Java 1.5開始加入的語言特性,在Java 1.5之前很長一段時間,Java程序已經在我們身邊的個人設備中運行。為了支持泛型出現以前的運行在Java開發的軟件中的各種庫,Java語言的設計師采取一種折中的方案,Java泛型不僅能向后兼容,而且保證現有的代碼和類文件也合法,仍然保持原來的意思。
Java泛型實現機制
Java泛型為了向前兼容,采取運行期類型擦出泛型參數的方式來實現。這就意味着,你在使用泛型的時候,任何具體的類型都已經被擦除。因此,以上的ArrayList<String>和ArrayList<Integer>實際上都恢復到他們的原生類型List,是同一種類型。正確理解Java泛型的類型擦除,能幫我們理解Java泛型中的很多奇怪特性。今天關於泛型就先寫到這里,下一節開始介紹Java泛型的類型的擦除機制引起的很多奇怪泛型特性、Java泛型設計做出的一些彌補。