本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html
![]()
以前章節中咱們屢次提到過泛型這個概念,從本節開始,咱們就來詳細討論Java中的泛型,雖然泛型的基本思惟和概念是比較簡單的,但它有一些很是使人費解的語法、細節、以及侷限性,內容比較多。java
因此咱們分爲三節,逐步來討論,本節咱們主要來介紹泛型的基本概念和原理,下節咱們重點討論使人費解的通配符,最後一節,咱們討論一些細節和泛型的侷限性。算法
後續章節咱們會介紹各類容器類,容器類能夠說是平常程序開發中每天用到的,沒有容器類,不可思議能開發什麼真正有用的程序。而容器類是基於泛型的,不理解泛型,咱們就難以深入理解容器類。那,泛型究竟是什麼呢?編程
以前咱們一直強調數據類型的概念,Java有8種基本類型,能夠定義類,類至關於自定義數據類型,類之間還能夠有組合和繼承。不過,在第19節,咱們介紹了接口,其中提到,其實,不少時候,咱們關心的不是類型,而是能力,針對接口和能力編程,不只能夠複用代碼,還能夠下降耦合,提升靈活性。數組
泛型將接口的概念進一步延伸,"泛型"字面意思就是普遍的類型,類、接口和方法代碼能夠應用於很是普遍的類型,代碼與它們可以操做的數據類型再也不綁定在一塊兒,同一套代碼,能夠用於多種數據類型,這樣,不只能夠複用代碼,下降耦合,同時,還能夠提升代碼的可讀性和安全性。安全
這麼說可能比較抽象,接下來,咱們經過一些例子逐步來講明。在Java中,類、接口、方法均可以是泛型的,咱們先來看泛型類。微信
咱們經過一個簡單的例子來講明泛型類的基本概念、實現原理和好處。數據結構
咱們直接來看代碼:dom
public class Pair<T> {
T first;
T second;
public Pair(T first, T second){
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public T getSecond() {
return second;
}
}
複製代碼
Pair就是一個泛型類,與普通類的區別,體如今:數據結構和算法
T是什麼呢?T表示類型參數,泛型就是類型參數化,處理的數據類型不是固定的,而是能夠做爲參數傳入。
怎麼用這個泛型類,並傳遞類型參數呢?看代碼:
Pair<Integer> minmax = new Pair<Integer>(1,100);
Integer min = minmax.getFirst();
Integer max = minmax.getSecond();
複製代碼
Pair<Integer>
,這裏Integer就是傳遞的實際類型參數。
Pair類的代碼和它處理的數據類型不是綁定的,具體類型能夠變化。上面是Integer,也能夠是String,好比:
Pair<String> kv = new Pair<String>("name","老馬");
複製代碼
類型參數能夠有多個,Pair類中的first和second能夠是不一樣的類型,多個類型之間以逗號分隔,來看改進後的Pair類定義:
public class Pair<U, V> {
U first;
V second;
public Pair(U first, V second){
this.first = first;
this.second = second;
}
public U getFirst() {
return first;
}
public V getSecond() {
return second;
}
}
複製代碼
能夠這樣使用:
Pair<String,Integer> pair = new Pair<String,Integer>("老馬",100);
複製代碼
<String,Integer>
既出如今了聲明變量時,也出如今了new後面,比較囉嗦,Java支持省略後面的類型參數,能夠這樣:
Pair<String,Integer> pair = new Pair<>("老馬",100);
複製代碼
泛型類型參數究竟是什麼呢?爲何必定要定義類型參數呢?定義普通類,直接使用Object不就好了嗎?好比,Pair類能夠寫爲:
public class Pair {
Object first;
Object second;
public Pair(Object first, Object second){
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public Object getSecond() {
return second;
}
}
複製代碼
使用Pair的代碼能夠爲:
Pair minmax = new Pair(1,100);
Integer min = (Integer)minmax.getFirst();
Integer max = (Integer)minmax.getSecond();
Pair kv = new Pair("name","老馬");
String key = (String)kv.getFirst();
String value = (String)kv.getSecond();
複製代碼
這樣是能夠的。實際上,Java泛型的內部原理就是這樣的。
咱們知道,Java有Java編譯器和Java虛擬機,編譯器將Java源代碼轉換爲.class文件,虛擬機加載並運行.class文件。對於泛型類,Java編譯器會將泛型代碼轉換爲普通的非泛型代碼,就像上面的普通Pair類代碼及其使用代碼同樣,將類型參數T擦除,替換爲Object,插入必要的強制類型轉換。Java虛擬機實際執行的時候,它是不知道泛型這回事的,它只知道普通的類及代碼。
再強調一下,Java泛型是經過擦除實現的,類定義中的類型參數如T會被替換爲Object,在程序運行過程當中,不知道泛型的實際類型參數,好比Pair<Integer>
,運行中只知道Pair,而不知道Integer,認識到這一點是很是重要的,它有助於咱們理解Java泛型的不少限制。
Java爲何要這麼設計呢?泛型是Java 1.5之後才支持的,這麼設計是爲了兼容性而不得已的一個選擇。
既然只使用普通類和Object就是能夠的,並且泛型最後也轉換爲了普通類,那爲何還要用泛型呢?或者說,泛型到底有什麼好處呢?
主要有兩個好處:
語言和程序設計的一個重要目標是將bug儘可能消滅在搖籃裏,能消滅在寫代碼的時候,就不要等到代碼寫完,程序運行的時候。
只使用Object,代碼寫錯的時候,開發環境和編譯器不能幫咱們發現問題,看代碼:
Pair pair = new Pair("老馬",1);
Integer id = (Integer)pair.getFirst();
String name = (String)pair.getSecond();
複製代碼
看出問題了嗎?寫代碼時,不當心,類型弄錯了,不過,代碼編譯時是沒有任何問題的,但,運行時,程序拋出了類型轉換異常ClassCastException。
若是使用泛型,則不可能犯這個錯誤,若是這麼寫代碼:
Pair<String,Integer> pair = new Pair<>("老馬",1);
Integer id = pair.getFirst();
String name = pair.getSecond();
複製代碼
開發環境如Eclipse會提示你類型錯誤,即便沒有好的開發環境,編譯時,Java編譯器也會提示你。這稱之爲類型安全,也就是說,經過使用泛型,開發環境和編譯器能確保你不會用錯類型,爲你的程序多設置一道安全防禦網。
使用泛型,還能夠省去繁瑣的強制類型轉換,再加上明確的類型信息,代碼可讀性也會更好。
泛型類最多見的用途是做爲容器類,所謂容器類,簡單的說,就是容納並管理多項數據的類。數組就是用來管理多項數據的,但數組有不少限制,好比說,長度固定,插入、刪除操做效率比較低。計算機技術有一門課程叫數據結構,專門討論管理數據的各類方式。
這些數據結構在Java中的實現主要就是Java中的各類容器類,甚至,Java泛型的引入主要也是爲了更好的支持Java容器。後續章節咱們會詳細討論主要的Java容器,本節咱們先本身實現一個很是簡單的Java容器,來解釋泛型的一些概念。
咱們來實現一個簡單的動態數組容器,所謂動態數組,就是長度可變的數組,底層數組的長度固然是不可變的,但咱們提供一個類,對這個類的使用者而言,好像就是一個長度可變的數組,Java容器中有一個對應的類ArrayList,本節咱們來實現一個簡化版。
來看代碼:
public class DynamicArray<E> {
private static final int DEFAULT_CAPACITY = 10;
private int size;
private Object[] elementData;
public DynamicArray() {
this.elementData = new Object[DEFAULT_CAPACITY];
}
private void ensureCapacity(int minCapacity) {
int oldCapacity = elementData.length;
if(oldCapacity>=minCapacity){
return;
}
int newCapacity = oldCapacity * 2;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
public void add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
}
public E get(int index) {
return (E)elementData[index];
}
public int size() {
return size;
}
public E set(int index, E element) {
E oldValue = get(index);
elementData[index] = element;
return oldValue;
}
}
複製代碼
DynamicArray就是一個動態數組,內部代碼與咱們以前分析過的StringBuilder相似,經過ensureCapacity方法來根據須要擴展數組。做爲一個容器類,它容納的數據類型是做爲參數傳遞過來的,好比說,存放Double類型:
DynamicArray<Double> arr = new DynamicArray<Double>();
Random rnd = new Random();
int size = 1+rnd.nextInt(100);
for(int i=0; i<size; i++){
arr.add(Math.random());
}
Double d = arr.get(rnd.nextInt(size));
複製代碼
這就是一個簡單的容器類,適用於各類數據類型,且類型安全。本節後面和後面兩節還會以DynamicArray爲例進行擴展,以解釋泛型概念。
具體的類型還能夠是一個泛型類,好比,能夠這樣寫:
DynamicArray<Pair<Integer,String>> arr = new DynamicArray<>()
複製代碼
arr表示一個動態數組,每一個元素是Pair<Integer,String>
類型。
除了泛型類,方法也能夠是泛型的,並且,一個方法是否是泛型的,與它所在的類是否是泛型沒有什麼關係。
咱們看個例子:
public static <T> int indexOf(T[] arr, T elm){
for(int i=0; i<arr.length; i++){
if(arr[i].equals(elm)){
return i;
}
}
return -1;
}
複製代碼
這個方法就是一個泛型方法,類型參數爲T,放在返回值前面,它能夠這麼調用:
indexOf(new Integer[]{1,3,5}, 10)
複製代碼
也能夠這麼調用:
indexOf(new String[]{"hello","老馬","編程"}, "老馬")
複製代碼
indexOf表示一個算法,在給定數組中尋找某一個元素,這個算法的基本過程與具體數據類型沒有什麼關係,經過泛型,它就能夠方便的應用於各類數據類型,且編譯器保證類型安全。
與泛型類同樣,類型參數能夠有多個,多個以逗號分隔,好比:
public static <U,V> Pair<U,V> makePair(U first, V second){
Pair<U,V> pair = new Pair<>(first, second);
return pair;
}
複製代碼
與泛型類不一樣,調用方法時通常並不須要特地指定類型參數的實際類型是什麼,好比調用makePair:
makePair(1,"老馬");
複製代碼
並不須要告訴編譯器U的類型是Integer,V的類型是String,Java編譯器能夠自動推斷出來。
接口也能夠是泛型的,咱們以前介紹過的Comparable和Comparator接口都是泛型的,它們的代碼以下:
public interface Comparable<T> {
public int compareTo(T o);
}
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
複製代碼
與前面同樣,T是類型參數。實現接口時,應該指定具體的類型,好比,對Integer類,實現代碼是:
public final class Integer extends Number implements Comparable<Integer>{
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
//...
}
複製代碼
經過implements Comparable<Integer>
,Integer實現了Comparable接口,指定了實際類型參數爲Integer,表示Integer只能與Integer對象進行比較。
再看Comparator的一個例子,String類內部一個Comparator的接口實現爲:
private static class CaseInsensitiveComparator implements Comparator<String> {
public int compare(String s1, String s2) {
//....
}
}
複製代碼
這裏,指定了實際類型參數爲String。
在以前的介紹中,不管是泛型類、泛型方法仍是泛型接口,關於類型參數,咱們都知之甚少,只能把它當作Object,但Java支持限定這個參數的一個上界,也就是說,參數必須爲給定的上界類型或其子類型,這個限定是經過extends這個關鍵字來表示的。
這個上界能夠是某個具體的類,或者某個具體的接口,也能夠是其餘的類型參數,咱們逐個來看下其應用。
好比說,上面的Pair類,能夠定義一個子類NumberPair,限定兩個類型參數必須爲Number,代碼以下:
public class NumberPair<U extends Number, V extends Number> extends Pair<U, V> {
public NumberPair(U first, V second) {
super(first, second);
}
}
複製代碼
限定類型後,就可使用該類型的方法了,好比說,對於NumberPair類,first和second變量就能夠當作Number進行處理了,好比能夠定義一個求和方法,以下所示:
public double sum(){
return getFirst().doubleValue()
+getSecond().doubleValue();
}
複製代碼
能夠這麼用:
NumberPair<Integer, Double> pair = new NumberPair<>(10, 12.34);
double sum = pair.sum();
複製代碼
限定類型後,若是類型使用錯誤,編譯器會提示。
指定邊界後,類型擦除時就不會轉換爲Object了,而是會轉換爲它的邊界類型,這也是容易理解的。
在泛型方法中,一種常見的場景是限定類型必須實現Comparable接口,咱們來看代碼:
public static <T extends Comparable> T max(T[] arr){
T max = arr[0];
for(int i=1; i<arr.length; i++){
if(arr[i].compareTo(max)>0){
max = arr[i];
}
}
return max;
}
複製代碼
max方法計算一個泛型數組中的最大值,計算最大值須要進行元素之間的比較,要求元素實現Comparable接口,因此給類型參數設置了一個上邊界Comparable,T必須實現Comparable接口。
不過,直接這麼寫代碼,Java中會給一個警告信息,由於Comparable是一個泛型接口,它也須要一個類型參數,因此完整的方法聲明應該是:
public static <T extends Comparable<T>> T max(T[] arr){
//...
}
複製代碼
<T extends Comparable<T>>
是一種使人費解的語法形式,這種形式稱之爲遞歸類型限制,能夠這麼解讀,T表示一種數據類型,必須實現Comparable接口,且必須能夠與相同類型的元素進行比較。
上面的限定都是指定了一個明確的類或接口,Java支持一個類型參數以另外一個類型參數做爲上界。爲何須要這個呢?
咱們看個例子,給上面的DynamicArray類增長一個實例方法addAll,這個方法將參數容器中的全部元素都添加到當前容器裏來,直覺上,代碼能夠這麼寫:
public void addAll(DynamicArray<E> c) {
for(int i=0; i<c.size; i++){
add(c.get(i));
}
}
複製代碼
但這麼寫有一些侷限性,咱們看使用它的代碼:
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);
複製代碼
numbers是一個Number類型的容器,ints是一個Integer類型的容器,咱們但願將ints添加到numbers中,由於Integer是Number的子類,應該說,這是一個合理的需求和操做。
但,Java會在number.addAll(ints)
這行代碼上提示編譯錯誤,提示,addAll須要的參數類型爲DynamicArray<Number>
,而傳遞過來的參數類型爲DynamicArray<Integer>
,不適用,Integer是Number的子類,怎麼會不適用呢?
事實就是這樣,確實不適用,並且是頗有道理的,假設適用,咱們看下會發生什麼。
DynamicArray<Integer> ints = new DynamicArray<>();
//假設下面這行是合法的
DynamicArray<Number> numbers = ints;
numbers.add(new Double(12.34));
複製代碼
那最後一行就是合法的,這時,DynamicArray<Integer>
中就會出現Double類型的值,而這,顯然就破壞了Java泛型關於類型安全的保證。
咱們強調一下,雖然Integer是Number的子類,但DynamicArray<Integer>
並非DynamicArray<Number>
的子類,DynamicArray<Integer>
的對象也不能賦值給DynamicArray<Number>
的變量,這一點初看上去是違反直覺的,但這是事實,必需要理解這一點。
不過,咱們的需求是合理的啊,將Integer添加到Number容器中,這沒有問題啊。這個問題,能夠經過類型限定,這樣來解決:
public <T extends E> void addAll(DynamicArray<T> c) {
for(int i=0; i<c.size; i++){
add(c.get(i));
}
}
複製代碼
E是DynamicArray的類型參數,T是addAll的類型參數,T的上界限定爲E,這樣,下面的代碼就沒有問題了:
DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);
複製代碼
對於這個例子,這個寫法有點囉嗦,下節咱們會看到一種簡化的方式。
泛型是計算機程序中一種重要的思惟方式,它將數據結構和算法與數據類型相分離,使得同一套數據結構和算法,可以應用於各類數據類型,並且還能夠保證類型安全,提升可讀性。在Java中,泛型普遍應用於各類容器類中,理解泛型是深入理解容器的基礎。
本節介紹了泛型的基本概念,包括泛型類、泛型方法和泛型接口,關於類型參數,咱們介紹了多種上界限定,限定爲某具體類、某具體接口、或其餘類型參數。泛型類最多見的用途是容器類,咱們實現了一個簡單的容器類DynamicArray,以解釋泛型概念。
在Java中,泛型是經過類型擦除來實現的,它是Java編譯器的概念,Java虛擬機運行時對泛型基本一無所知,理解這一點是很重要的,它有助於咱們理解Java泛型的不少侷限性。
關於泛型,Java中有一個通配符的概念,語法很是使人費解,並且容易混淆,下一節,咱們力圖對它進行清晰的剖析。
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。