一、什么是泛型?
泛型支持在定義類、接口和方法時將類型作為參數(shù)。泛型參數(shù)與方法聲明中使用的形式參數(shù)非常相似,泛型參數(shù)使我們可以在不同的輸入中重用相同的代碼。但與形式參數(shù)不同之處在于,形式參數(shù)的輸入是值,而類型參數(shù)的輸入是類型。
我們可以將泛型 < T > 理解為占位符,在定義類時不清楚它會是什么類型,但是類中的代碼邏輯是通用的,無論類在使用時< T >會被什么參數(shù)類型替代。
比如我們經(jīng)常使用的List集合,在創(chuàng)建時通常都要指定其類型:
//指定泛型類型為String
List<String> stringList = new ArrayList<>();
stringList.add("hello");
stringList.add("world");
//試圖添加非String類型元素,編譯會報(bào)錯
stringList.add(100);
當(dāng)然我們也可以不指定集合泛型E,但是它會帶來隱患:
//未指定元素泛型類型,可以存儲任何object類型
List frulist = new ArrayList();
frulist .add("apple");
frulist .add("banana");
//隱患1:可以加入其它類型元素
frulist.add(100);
//隱患2:取出元素時,必須進(jìn)行類型轉(zhuǎn)換,容易出錯
String str = (String)frulist.get(0);
二、泛型的定義
上面已經(jīng)提到,泛型支持在定義類、接口和方法時將類型作為參數(shù)。下面我們通過例子來看下具體泛型具體使用方式。
(1)泛型類
泛型類的定義方式如下,類型參數(shù) T由< >包裹,緊跟在類名之后,類型參數(shù)可以有多個,以英文逗號分割。
class name<T1, T2, ..., Tn> {
/* ... */
}
知道了泛型類的格式,我們來具體實(shí)踐下,先定義一個非泛型的類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類型,所以我們可以自由的存放任何類型數(shù)據(jù),在使用時可能會像下面這樣:
public static void main(String[] args) {
//創(chuàng)建一個box類,來存放String類型數(shù)據(jù)
Box box = new Box();
box.set("hello world");
//取值時,都要進(jìn)行強(qiáng)制類型轉(zhuǎn)換
String content = (String) box.get();
//.....很多行代碼后,不小心存了boolean類型
box.set(false);
//...很多行代碼后,又一個疏忽,帶來了ClassCastException
Integer count = (Integer) box.get();
}
可以看到,在使用非泛型的Box類時,雖然存放的元素類型非常自由,但也存在很多嚴(yán)重問題,比如我們創(chuàng)建Box類對象,本來是想存放String類型數(shù)據(jù),卻可能不小心使用box的set()方法存了boolean類型,另外每次使用box.get()取值時,都要進(jìn)行強(qiáng)制類型轉(zhuǎn)換,很容易遇見java.lang.ClassCastException
這時候我們就可以使用泛型類了,對Box類進(jìn)行改造,在類的聲明時加入泛型參數(shù) < T >,然后在Box類內(nèi)部就可以使用泛型參數(shù) T 代替原來的Object,set\get方法所使用的參數(shù)類型,也都使用 T 來代替,此時我們的Box類就是泛型類了。
class Box <T> {
private T content;
public void set(T object) {
this.content = object;
}
public T get() {
return content;
}
}
這時我們在使用泛型類Box時,指定類型參數(shù)< T >的實(shí)際類型即可
public static void main(String[] args) {
//創(chuàng)建Box類時,指定泛型參數(shù)為String
Box<String> box = new Box();
box.set("hello world");
//由于指定了泛型,不需要在進(jìn)行強(qiáng)制類型轉(zhuǎn)換
String content = box.get();
//不小心存了boolean類型,IDE在編譯時會報(bào)錯
box.set(false);
}
到了這里你是否聯(lián)想到,我們經(jīng)常使用的集合List< T >,Map < K,V>,例如HashMap的源碼中類聲明部分:
//HashMap類源碼
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//........../
}
(2)泛型方法
泛型方法是引入自己的類型參數(shù)的方法。類似于聲明泛型類型,但是類型參數(shù)的范圍僅限于聲明它的方法。允許使用靜態(tài)和非靜態(tài)泛型方法,以及泛型類構(gòu)造函數(shù)。
泛型方法的語法包括尖括號< >內(nèi)的類型參數(shù)列表,該列表出現(xiàn)在方法的返回類型之前。對于靜態(tài)泛型方法,類型參數(shù)部分必須出現(xiàn)在方法的返回類型之前。
泛型方法長什么樣子呢,下面看個例子:
/**
*泛型類Pair,用于創(chuàng)建key-value類型鍵值對
*類似與JDK中Map內(nèi)部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
* 泛型參數(shù)列表<T>出現(xiàn)在必須位于方法的返回值之前。
* 泛型參數(shù)<T>在聲明后,才能在方法內(nèi)部使用
* 泛型類中的返回值為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());
}
/**
*泛型方法,
*計(jì)算數(shù)組T[]中大于指定元素elem的元素?cái)?shù)量
* <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");
//使用泛型方法,并指定參數(shù)類型
boolean same = Util.<Integer, String>compare(p1, p2);
}
}
三、泛型規(guī)范
(1)類型參數(shù)命名約定
按照慣例,類型參數(shù)名稱是單個大寫字母。便于區(qū)分類型變量和普通類或接口名稱。最常用的類型參數(shù)名有:
T 類型,常用在泛型類、泛型接口上,如java 中的 Comparable< T >
E 元素,在Java集合類中廣泛使用,如List< E > , Set< E >
N 數(shù)值類型,主要用在數(shù)字相關(guān)的
K 鍵,典型的就是Map< K ,V>
V 值,同Map< K ,V>
以上是官方推薦的幾種,除此之外,還可以使用S、U等等
(2)泛型通配符與泛型限定
在泛型里代碼中,使用 <?> 作為通配符,表示一個未知的類型。
那為什么要使用通配符呢?主要是因?yàn)樵趈ava中,數(shù)組是可以協(xié)變的,比如Cat extends Animal,那么Animal[] 與Cat[]是兼容的。而集合是不能協(xié)變的,也就是說List < Animal >不是List< Cat >的父類,二者不能相互賦值,這就導(dǎo)致了一個邏輯上問——能夠存放父類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父類,數(shù)組可以賦值
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)
<? extends E>, E指是就是該泛型的上邊界,表示泛型的類型只能是E類或者E類的子類 ,這里雖然用的是extends關(guān)鍵字, 卻不僅限于繼承了E的子類, 也可以代指接口E的實(shí)現(xiàn)類。
public static void main(String[] args) {
// <? extends Animal>限定了泛型類只能是Animal或其子類對象的List
List<? extends Animal> animals = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
//下面代碼不會報(bào)錯,二者引用可以賦值
animals = cats;
//但是集合中無法add元素,因?yàn)闊o法確定持有的實(shí)際類型,
animals.add(new Cat());//error
//從集合中獲取對象是是可以的,因?yàn)樵谶@個List中,不管實(shí)際類型是什么,肯定都能轉(zhuǎn)型為Animal
Animal animal = animals.get(0);
}
下邊界限定的通配符(Lower Bounded wildcards)
<? super E>,表示泛型的類型只能是E類或者E類的父類,List<? super Integer> list表示某種特定類型(Integer或者Integer的父類)對象的List。可以確定這個List持有的對象類型肯定是Integer或者其父類。
//某種特定類型(Integer或者Integer的父類)對象的List
List<? super Integer> list = new ArrayList<>();
//往list里面add一個Integer或者其子類的對象是安全的,
//因?yàn)镮nteger或者其子類的對象,都可以向上轉(zhuǎn)型為Integer的父類對象
list.add(new Integer(1));
//下面代碼編譯錯誤,因?yàn)闊o法確定實(shí)際類型,所以往list里面add一個Integer的父類對象是不被允許的
list.add(new Object());
所以從上面上邊界限定的通配符和下邊界限定的通配符的特性,可以知道:
對于上邊界限定的通配符 <? extends E>,無法向其中加入任何對象,但是可以從中正常取出對象。
對于下邊界限定的通配符 <? super E>,,可以存入subclass對象或者subclass的子類對象,但是取出時只能用Object類型變量指向取出的對象。
四、類型擦除
Java 中的的泛型是偽泛型,這是因?yàn)榉盒托畔⒅淮嬖谟诖a編譯階段,編譯后與泛型相關(guān)的信息會被擦除掉,稱為類型擦除(type erasure)。
編譯器在編譯期,會將泛型轉(zhuǎn)化為原生類型。并在相應(yīng)的地方插入強(qiáng)制轉(zhuǎn)型的代碼。什么是原生類型呢?原生類型就是刪去類型參數(shù)后泛型類的類型名,比如:
List<String> stringList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();
//下面的結(jié)果為true,因?yàn)轭愋筒脸螅咴愋投际荓ist
System.out.println(stringList.getClass() == integerList.getClass());
如果泛型參數(shù)中,有限定符則會使用 第一個限定符的類型來替換,比如
class Box <T extends Number> {
private T content;
}
類型擦除后的原生類型變?yōu)槠湎薅ǚ愋停?br />
class Box{
private Number content;
}
本站文章版權(quán)歸原作者及原出處所有 。內(nèi)容為作者個人觀點(diǎn), 并不代表本站贊同其觀點(diǎn)和對其真實(shí)性負(fù)責(zé),本站只提供參考并不構(gòu)成任何投資及應(yīng)用建議。本站是一個個人學(xué)習(xí)交流的平臺,網(wǎng)站上部分文章為轉(zhuǎn)載,并不用于任何商業(yè)目的,我們已經(jīng)盡可能的對作者和來源進(jìn)行了通告,但是能力有限或疏忽,造成漏登,請及時聯(lián)系我們,我們將根據(jù)著作權(quán)人的要求,立即更正或者刪除有關(guān)內(nèi)容。本站擁有對此聲明的最終解釋權(quán)。