一、什麼是泛型?
泛型支援在定義類、介面和方法時將型別作為引數。泛型引數與方法宣告中使用的形式引數非常相似,泛型引數使我們可以在不同的輸入中重用相同的程式碼。但與形式引數不同之處在於,形式引數的輸入是值,而型別引數的輸入是型別。 我們可以將泛型 < T > 理解為佔位符,在定義類時不清楚它會是什麼型別,但是類中的程式碼邏輯是通用的,無論類在使用時< T >會被什麼引數型別替代。
比如我們經常使用的List集合,在建立時通常都要指定其型別:
//指定泛型型別為String
List<String> stringList = new ArrayList<>();
stringList.add("hello");
stringList.add("world");
//試圖新增非String型別元素,編譯會報錯
stringList.add(100);
複製程式碼
當然我們也可以不指定集合泛型E,但是它會帶來隱患:
//未指定元素泛型型別,可以儲存任何object型別
List frulist = new ArrayList();
frulist .add("apple");
frulist .add("banana");
//隱患1:可以加入其它型別元素
frulist.add(100);
//隱患2:取出元素時,必須進行型別轉換,容易出錯
String str = (String)frulist.get(0);
複製程式碼
二、泛型的定義
上面已經提到,泛型支援在定義類、介面和方法時將型別作為引數。下面我們通過例子來看下具體泛型具體使用方式。
(1)泛型類
泛型類的定義方式如下,型別引數 T由< >包裹,緊跟在類名之後,型別引數可以有多個,以英文逗號分割。
class name<T1, T2, ..., Tn> {
/* ... */
}
複製程式碼
知道了泛型類的格式,我們來具體實踐下,先定義一個非泛型的類Box,它只有一個Object型別成員變數,同時提供簡單的set\get方法
class Box{
private Object content;
public void set(Object object) {
this.content = object;
}
public Object get() {
return content;
}
}
複製程式碼
Box類的成員屬性content是Object型別,所以我們可以自由的存放任何型別資料,在使用時可能會像下面這樣:
public static void main(String[] args) {
//建立一個box類,來存放String型別資料
Box box = new Box();
box.set("hello world");
//取值時,都要進行強制型別轉換
String content = (String) box.get();
//.....很多行程式碼後,不小心存了boolean型別
box.set(false);
//...很多行程式碼後,又一個疏忽,帶來了ClassCastException
Integer count = (Integer) box.get();
}
複製程式碼
可以看到,在使用非泛型的Box類時,雖然存放的元素型別非常自由,但也存在很多嚴重問題,比如我們建立Box類物件,本來是想存放String型別資料,卻可能不小心使用box的set()方法存了boolean型別,另外每次使用box.get()取值時,都要進行強制型別轉換,很容易遇見java.lang.ClassCastException
這時候我們就可以使用泛型類了,對Box類進行改造,在類的宣告時加入泛型引數 < T >,然後在Box類內部就可以使用泛型引數 T 代替原來的Object,set\get方法所使用的引數型別,也都使用 T 來代替,此時我們的Box類就是泛型類了。
class Box <T> {
private T content;
public void set(T object) {
this.content = object;
}
public T get() {
return content;
}
}
複製程式碼
這時我們在使用泛型類Box時,指定型別引數< T >的實際型別即可
public static void main(String[] args) {
//建立Box類時,指定泛型引數為String
Box<String> box = new Box();
box.set("hello world");
//由於指定了泛型,不需要在進行強制型別轉換
String content = box.get();
//不小心存了boolean型別,IDE在編譯時會報錯
box.set(false);
}
複製程式碼
到了這裡你是否聯想到,我們經常使用的集合List< T >,Map < K,V>,例如HashMap的原始碼中類宣告部分:
//HashMap類原始碼
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//........../
}
複製程式碼
(2)泛型方法
泛型方法是引入自己的型別引數的方法。類似於宣告泛型型別,但是型別引數的範圍僅限於宣告它的方法。允許使用靜態和非靜態泛型方法,以及泛型類建構函式。 泛型方法的語法包括尖括號< >內的型別引數列表,該列表出現在方法的返回型別之前。對於靜態泛型方法,型別引數部分必須出現在方法的返回型別之前。 泛型方法長什麼樣子呢,下面看個例子:
/**
*泛型類Pair,用於建立key-value型別鍵值對
*類似與JDK中Map內部Entry<K,V>
*/
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
public class Util {
/**
* 泛型方法compare
* 泛型引數列表<T>出現在必須位於方法的返回值之前。
* 泛型引數<T>在宣告後,才能在方法內部使用
* 泛型類中的返回值為T的方法不是泛型方法
**/
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
/**
*泛型方法,
*計算陣列T[]中大於指定元素elem的元素數量
* <T extends Comparable<T> > 是泛型的繼承,extends 限定了方法
* 中使用的<T>必須是Comparable<T>的子類,這樣才能在方法裡使用compareTo方法
*/
public static <T extends Comparable<T> > int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
public static void main(String[] args) {
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
//使用泛型方法,並指定引數型別
boolean same = Util.<Integer, String>compare(p1, p2);
}
}
複製程式碼
三、泛型規範
(1)型別引數命名約定
按照慣例,型別引數名稱是單個大寫字母。便於區分型別變數和普通類或介面名稱。最常用的型別引數名有:
- T 型別,常用在泛型類、泛型介面上,如java 中的 Comparable< T >
- E 元素,在Java集合類中廣泛使用,如List< E > , Set< E >
- N 數值型別,主要用在數字相關的
- K 鍵,典型的就是Map< K ,V>
- V 值,同Map< K ,V>
以上是官方推薦的幾種,除此之外,還可以使用S、U等等
(2)泛型萬用字元與泛型限定
在泛型裡程式碼中,使用 <?> 作為萬用字元,表示一個未知的型別。 那為什麼要使用萬用字元呢?主要是因為在java中,陣列是可以協變的,比如Cat extends Animal,那麼Animal[] 與Cat[]是相容的。而集合是不能協變的,也就是說List < Animal >不是List< Cat >的父類,二者不能相互賦值,這就導致了一個邏輯上問——能夠存放父類Animal元素的集合,卻不能存放它的子類Cat。
abstract class Animal {
public abstract void eat();
}
class Cat extends Animal{
@Override
public void eat() {}
}
public class TestC{
public static void main(String[] args) {
//Animal是Cat父類,陣列可以賦值
Animal[] animal = new Cat[5];
//Animal是Cat父類,但是不意味著List<Animal>集合是List<Cat>父類,
List<Animal> animals = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
//下面這行程式碼會編譯失敗,編譯器無法推斷出List<Cat>是List<Animal>的子類
animals = cats; // incompatible types
}
}
複製程式碼
為了解決上面描述的問題,泛型的萬用字元就派上用途了。泛型萬用字元分為三種型別:
-
無邊界配符(Unbounded Wildcards) < ? >就是無邊界萬用字元,比如
List<?> list
表示持有某種特定型別物件的List,但是不知道是哪種型別,所以不能add任何型別的物件。它與List list
並不相同,List list
是表示持有Object型別物件的List,可以add任何型別的物件。 -
上邊界限定的萬用字元(Upper Bounded wildcards)
, E指是就是該泛型的上邊界,表示泛型的型別只能是E類或者E類的子類 ,這裡雖然用的是extends關鍵字, 卻不僅限於繼承了E的子類, 也可以代指介面E的實現類。
public static void main(String[] args) {
// <? extends Animal>限定了泛型類只能是Animal或其子類物件的List
List<? extends Animal> animals = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
//下面程式碼不會報錯,二者引用可以賦值
animals = cats;
//但是集合中無法add元素,因為無法確定持有的實際型別,
animals.add(new Cat());//error
//從集合中獲取物件是是可以的,因為在這個List中,不管實際型別是什麼,肯定都能轉型為Animal
Animal animal = animals.get(0);
}
複製程式碼
-
下邊界限定的萬用字元(Lower Bounded wildcards)
,表示泛型的型別只能是E類或者E類的父類,List list表示某種特定型別(Integer或者Integer的父類)物件的List。可以確定這個List持有的物件型別肯定是Integer或者其父類。
//某種特定型別(Integer或者Integer的父類)物件的List
List<? super Integer> list = new ArrayList<>();
//往list裡面add一個Integer或者其子類的物件是安全的,
//因為Integer或者其子類的物件,都可以向上轉型為Integer的父類物件
list.add(new Integer(1));
//下面程式碼編譯錯誤,因為無法確定實際型別,所以往list裡面add一個Integer的父類物件是不被允許的
list.add(new Object());
複製程式碼
所以從上面上邊界限定的萬用字元和下邊界限定的萬用字元的特性,可以知道:
- 對於上邊界限定的萬用字元 <? extends E>,無法向其中加入任何物件,但是可以從中正常取出物件。
- 對於下邊界限定的萬用字元 <? super E>,,可以存入subclass物件或者subclass的子類物件,但是取出時只能用Object型別變數指向取出的物件。
四、型別擦除
Java 中的的泛型是偽泛型,這是因為泛型資訊只存在於程式碼編譯階段,編譯後與泛型相關的資訊會被擦除掉,稱為型別擦除(type erasure)。 編譯器在編譯期,會將泛型轉化為原生型別。並在相應的地方插入強制轉型的程式碼。什麼是原生型別呢?原生型別就是刪去型別引數後泛型類的型別名,比如:
List<String> stringList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();
//下面的結果為true,因為型別擦除後,二者原生型別都是List
System.out.println(stringList.getClass() == integerList.getClass());
複製程式碼
如果泛型引數中,有限定符則會使用 第一個限定符的型別來替換,比如
class Box <T extends Number> {
private T content;
}
複製程式碼
型別擦除後的原生型別變為其限定符型別:
class Box{
private Number content;
}
複製程式碼
總結
泛型所涵蓋內容不止這麼多,希望通過本文對你瞭解JAVA中泛型有所幫助。