本文出自伯特的《LoulanPlan》,轉載務必注明作者及出處。
對于 Java 開發者而言,泛型是必須掌握的知識點。泛型本身并不復雜,但由于涉及的概念、用法較多,所以打算通過系列文章去講解,旨在全面、通俗的介紹泛型及其使用。如果你是初學者,可以通過本文了解泛型,并滿足企業級開發的需求;如果你對泛型已有一定的了解,可以通過本文進行鞏固,加深對泛型的理解。
作為系列文章的第一篇,本文將帶你了解 Java 泛型的前生今世,看看泛型的誕生之于開發者的意義。
1. 泛型之前:通用數據類型
對于集合框架中的 List
及其實現類,想必大家都不陌生。同時,泛型誕生之后即被廣泛運用于 Java 集合框架。所以,我們就以 List
作為觀察對象,看看在泛型誕生之前,Oracel 的工程師們是如何進行設計的。
摘自 JDK 1.4 的 List.java
源碼:
public interface List extends Collection {
//添加元素
boolean add(Object o);
//查詢元素
Object get(int index);
}
可以看出 List
是通過 Object
類型管理的數據,如此設計的好處顯而易見:
具備通用性,因為所有的類都是 Object 的直接或間接子類,所以適用于任意類型的對象。
同時,弊端也是不可忽視的。下面就通過使用 List
存、取數據來看看都有哪些問題:
//構造對象
List list = new ArrayList();
//存
list.add(1);
list.add("2");//①
//取
int num1 = (int)list.get(0);
int num2 = (int)list.get(1);//②
由于使用 Object
,編譯器無法判斷存、取數據的實際類型,導致上述幾行代碼暴露出許多問題:
-
無法限制存儲數據類型,不夠健壯:在 ① 處可以添加
String
類型數據,顯然是臟數據; - 取出時強轉代碼冗余,可讀性差:取出數據時必須顯示強轉為 int 類型;
- 由于 ① 處在編譯時無法檢查出錯誤,導致 ② 處的強轉在運行時引發
ClassCastException
,安全性低;
問題還真不少!
2. 泛型萌芽:數據類型的包裝
上述問題究其根本,是無法限制數據類型引起的。也就是說,如果我們基于 List
包裝出相應類型的 XxxList
,就可以解決問題。
舉個例子,包裝用于存儲 Integer
數據類型的 IntegerList
:
public class IntegerList {
List list = new ArrayList();
//限制外部只能添加整型數據
public boolean add(Integer data) {
return list.add(data);
}
//內部進行強轉,調用者可以直接賦值為整型
public Integer get(int index) {
return (Intrger)list.get(index);
}
}
包裝內依然使用 List
管理數據,但我們對外暴露的接口限制了數據類型,規避了直接訪問 List
的接口可能引發的問題。
下面一起來看看如何使用包裝類:
//構造對象
IntegerList list = new IntegerList();
//存
list.add(1);
list.add("2");//①
//取
int num1 = list.get(0);
怎么樣,一個包裝類輕松解決問題:
- 在 ① 處試圖添加
String
類型數據,會在編譯期進行類型檢查時報錯,導致編譯失敗; - 在取出數據時,無需重復強轉,直接賦值給 int 類型的數據;
- 因為限制了
add()
方法的參數類型,所以不用擔心在get()
時內部強轉會引發異常。
簡直完美。同理,可以包裝出一系列 StringList, LongList,以及自定義數據的集合包裝類 PeopleList, DataList 等。
但人無完人,類亦無完類啊。包裝類雖解決了編碼上的數據類型問題,可在工程效率方面卻捉襟見肘:
- 復用性低:每一個包裝類只適用于一種數據類型,無法復用核心邏輯;
- 維護成本高:復用性低必然會增加后期維護的成本。
仍需努力!
3. 泛型登場:參數化類型
雖然包裝類存在缺陷,但其對于理解泛型思想是很有意義的。不知 Oracle 的工程師們,是否受此啟發設計出的泛型呢?
如果你試著多寫幾個數據類型的包裝類,就會發現各包裝類之間的區別和聯系:
- 區別:數據類型不同;
- 聯系:操作數據的方法相同,即核心算法邏輯是一致的。
既然如此,如果我們能夠弱化數據類型,使其不再受具體的業務場景限制,就可以做到專注于通用的算法邏輯,從而提升復用性。
那么,如何弱化數據類型呢?有人說了,使用 Object 就很弱化啊。咳,麻煩你從頭開始看。。。
JDK 5(即 JDK 1.4 之后的 1.5) 引入了 泛型(Generic Type)
的概念,其通過“參數化類型”實現數據類型的弱化,使得程序內部不需要關心具體的數據類型,而是讓業務在調用時作為參數傳入。泛型將傳入的數據類型傳遞給編譯器,這樣編譯器就可以在編譯期間進行類型檢查,確保程序的安全性,并且可以插入相應的強轉以避免開發人員顯示強轉。
上面這段話值得多讀幾遍,尤其是“參數化類型”可以說是泛型的核心所在。如果還有點蒙沒關系,繼續往下看。
Java 中方法的聲明大家都不陌生,如果某個方法需要對整數進行加法運算,我們可以在聲明方法時添加整數類型的參數,外部調用時必須傳入相應的整數數據。這里,將數據抽象為參數的過程,可以理解為“參數化實參”。
那么,“參數化類型”可以理解為是“參數化數據”的進一步抽象:將數據類型抽象為參數,即類型形參。如此一來,數據類型可以像形參一樣,在調用時動態指定。如此,就達到了使用通用邏輯動態處理不同數據類型的目的。
下面,我們通過 JDK 源碼中有關泛型的運用來鞏固這一概念。
4. 泛型的簡單運用
泛型誕生后,即對 Java 集合框架進行了大刀闊斧的修改,引入了泛型。下面仍然以 List
作為觀察對象,看看泛型帶來了哪些改變。
//摘自 JDK 5 版本的 List 源碼
public interface List<E> extends Collection<E> {
//添加元素
boolean add(E e);
//指定下標查詢元素
E get(int index);
//指定下標移除元素
E remove(int index);
}
可以看出,List<E>
通過在類 List
后追加 <>
標識其為泛型類,包含的元素 E
即“類型形參“,以支持開發者在使用時指定實際類型。下面看看在代碼中如何使用泛型 List
:
//構造對象
List<Integer> list = new ArrayList();
//存
list.add(1);
list.add("2");//①
//取
int num1 = list.get(0);
int num2 = list.get(1);
首先,我們構造了 List<Integer>
類型的對象,所以在運行時 List<E>
中的形參會被當做 Integer
去出處理,我們可以想象出一個虛擬的 List
類:
public interface List extends Collection<E> {
boolean add(Integer e);
Integer get(int index);
Integer remove(int index);
}
接下來,和文章開頭一樣,我們對集合進行了相關操作,可以看出使用泛型解決了我們之前遇到的所有問題:
- ① 處的代碼在編譯期間會出錯:由于聲明的是
Integer
類型的List
,顯然無法接收String
類型的數據。 - 從虛擬
List
可以知道,取出元素時不需要顯示強轉,自然也不會在運行時拋出異常。
通過對泛型 List
的簡單運用,可以看出引入泛型后集合不失普適性,依然可以針對各種類型對象進行操作。同時,泛型為集合框架增加了編譯時類型安全性,并避免了在使用過程中的強轉操作。
5. 總結
有關泛型的前生今世就介紹到這兒了。至此,我們通過相關示例一步步引出了泛型,了解了泛型誕生前后在一些編碼場景下的差異。最后還通過實例簡單使用了泛型,但泛型的運用遠不止如此...
下一篇將進一步介紹泛型的各種運用場景,掌握泛型的用武之地。