泛型是Java
中一項(xiàng)十分重要的特性薄风,在Java 5
版本被引入昔搂,在日常的編程過(guò)程中句惯,有很多依賴(lài)泛型的場(chǎng)景土辩,尤其是在集合容器類(lèi)的使用過(guò)程中支救,更是離不開(kāi)泛型的影子抢野。
泛型的作用
泛型提供的功能有:參數(shù)化類(lèi)型,以及編譯期類(lèi)型檢查各墨。
1 參數(shù)化類(lèi)型
在方法的定義中指孤,方法的參數(shù)稱(chēng)為形參,在實(shí)際調(diào)用方法時(shí)傳遞實(shí)參。泛型的使用中恃轩,可以將類(lèi)型定義為一個(gè)參數(shù)结洼,在實(shí)際使用時(shí)再傳遞具體類(lèi)型。將泛型這種使用方式稱(chēng)之為參數(shù)化類(lèi)型叉跛。
在集合類(lèi)的使用中松忍,若不使用泛型,則需要對(duì)每一種元素類(lèi)型設(shè)計(jì)相同的集合操作筷厘,例如:
class ListInteger{
//...
}
class ListDouble{
//...
}
通過(guò)泛型的使用鸣峭,可以避免這種重復(fù)定義的現(xiàn)象,定義一套集合操作酥艳,來(lái)應(yīng)對(duì)所有元素類(lèi)型摊溶,例如:
class List<E>{
//...
}
在使用中傳遞不同的元素類(lèi)型給List
即可。
這里使用的字符
E
并無(wú)特殊含義充石,只是為了便于理解而已莫换。泛型中通常使用的字符及表示意義為:
K:
鍵值對(duì)中的key
V:
鍵值對(duì)中的value
E:
集合中的element
T:
類(lèi)的類(lèi)型type
2 編譯期類(lèi)型檢查
對(duì)于集合ArrayList
而言,若不指定具體元素類(lèi)型骤铃,則使用過(guò)程中可能出現(xiàn)以下情況:
List list = new ArrayList();
list.add("abc");
list.add(123);
for (Object obj : list) {
String e = (String) obj;//ClassCastException
}
這段代碼在編譯期沒(méi)問(wèn)題拉岁,運(yùn)行時(shí)會(huì)報(bào)出java.lang.ClassCastException
。
這種對(duì)集合的使用方式存在兩個(gè)問(wèn)題:一是add
添加元素時(shí)惰爬,因?yàn)樵芈暶鳛?code>Object類(lèi)型膛薛,任意類(lèi)型元素都可以添加到集合中,所以在添加元素時(shí)需要使用者自己注意選擇的元素類(lèi)型补鼻;二是get
取元素時(shí)需要強(qiáng)制類(lèi)型轉(zhuǎn)換哄啄,需要開(kāi)發(fā)人員記住操作的元素類(lèi)型,否則可能拋出ClassCastException
異常风范。
在聲明集合時(shí)指定元素類(lèi)型則可以避免以上兩種問(wèn)題:
List<String> list = new ArrayList<String>();
list.add("abc");
//list.add(123); compile error
for (String obj : list) {
String e = obj;
}
通過(guò)泛型的使用咨跌,指定集合元素的類(lèi)型,則可以在編譯期就進(jìn)行元素類(lèi)型檢查硼婿,并且get
獲取元素時(shí)無(wú)需進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換锌半。
這里稱(chēng)獲取元素?zé)o需進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換,其實(shí)并不準(zhǔn)確寇漫,嚴(yán)格來(lái)講刊殉,使用泛型在進(jìn)行獲取元素操作時(shí),進(jìn)行的是隱式類(lèi)型轉(zhuǎn)換州胳,所以仍然存在強(qiáng)制類(lèi)型轉(zhuǎn)換的操作记焊。
ArrayList
中的隱式類(lèi)型轉(zhuǎn)換:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
泛型的使用
泛型可以應(yīng)用于定義泛型類(lèi)、泛型接口和泛型方法栓撞。
1 泛型類(lèi)
泛型類(lèi)的定義方式較為簡(jiǎn)單遍膜,通過(guò)將類(lèi)型抽象為參數(shù)碗硬,附加在類(lèi)名稱(chēng)后,即可完成泛型類(lèi)的定義瓢颅,示例:
public class Test {
public static void main(String[] args) {
User<Integer> user = new User<>();
user.setAttribute(123);
// user.setAttribute("abc");compile error
Integer attribute = user.getAttribute();
}
}
class User<T> {
private T attribute;
public User() {
}
public T getAttribute() {
return this.attribute;
}
public void setAttribute(T attribute) {
this.attribute = attribute;
}
}
通過(guò)使用泛型類(lèi)恩尾,可以在編譯期進(jìn)行參數(shù)類(lèi)型檢查,并且使用時(shí)無(wú)需進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換挽懦。
2 泛型接口
泛型接口的使用與泛型類(lèi)較為相似翰意,在接口名稱(chēng)后添加表示類(lèi)型的字符即可,示例:
interface Person<T> {
T getAttribute();
void setAttribute(T attribute);
}
3 泛型方法
在前面的泛型類(lèi)中定義的如下方法:
public T getAttribute() {
return this.attribute;
}
public void setAttribute(T attribute) {
this.attribute = attribute;
}
雖然使用了參數(shù)化類(lèi)型信柿,但是并不算是泛型方法猎物,因?yàn)檫@些方法中使用的參數(shù)類(lèi)型是泛型類(lèi)定義的。泛型方法中定義了自己使用的類(lèi)型角塑,示例:
public <T> void genericsMethod(T parameter){
//...
}
泛型與繼承
在泛型的使用中蔫磨,關(guān)于繼承方面需要注意,示例:
public class Test {
public static void main(String[] args) {
A<Number> aNumber = new A<>();
A<Integer> aInteger = new A<>();
// aNumber = aInteger; compile error
System.out.println(aNumber.getClass() == aInteger.getClass()); // true
}
static class A<T>{}
}
雖然Integer
是Number
的子類(lèi)型圃伶,但是A<Integer>
并不是A<Number>
的子類(lèi)型堤如。
事實(shí)上,編譯器會(huì)在編譯階段進(jìn)行類(lèi)型檢查后窒朋,會(huì)擦除泛型的類(lèi)型信息搀罢,也就是說(shuō)在運(yùn)行期
A<Integer>
和A<Number>
是同一個(gè)類(lèi)。
對(duì)于泛型容器類(lèi)List<E>
侥猩,在進(jìn)行泛型擦除后榔至,記錄的元素類(lèi)型為其聲明的最左邊父類(lèi)型,此處即為Object
類(lèi)型欺劳,示例:
public class Test {
public static void main(String[] args) throws Exception {
List<Integer> integers = new ArrayList<>();
integers.getClass().getDeclaredMethod("add", Object.class).invoke(integers, "abc");
}
}
代碼在編譯期和運(yùn)行期都沒(méi)問(wèn)題唧取,在編譯生成的.class
文件中,Integer
元素類(lèi)型被擦除后划提,容器的元素類(lèi)型記錄為Object
類(lèi)型枫弟。
泛型使用中的繼承定義方式如下:
public class Test {
public static void main(String[] args) {
A<Integer> a = new A<>();
B<Integer> b = new B<>();
a = b;
}
}
class A<T>{}
class B<T> extends A<T>{}
在繼承關(guān)系中使用同一個(gè)參數(shù)類(lèi)型,以此實(shí)現(xiàn)泛型類(lèi)的繼承鹏往。在JDK
中ArrayList<E>
淡诗、List<E>
與Collection<E>
采用的就是這種方式。
但是這種繼承方式依然不能滿(mǎn)足前面提到的使用場(chǎng)景伊履,例如如下使用List
方式:
public class Test {
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
// numberList = integerList; compile error
}
}
雖然Integer
是Number
的子類(lèi)型韩容,但List<Integer>
卻不是List<Number>
的子類(lèi)型,問(wèn)題與前面的示例中相同唐瀑。
通配符
通配符號(hào)?
是一種實(shí)參類(lèi)型群凶,表示類(lèi)型不確定的意思,或者表示任意一種類(lèi)型介褥,選擇?
作為類(lèi)型的目的是為了匹配更大范圍的類(lèi)型座掘,所以這里?
是一種具體的類(lèi)型。
這里稱(chēng)
?
類(lèi)型不確定柔滔,又稱(chēng)?
是一種具體的類(lèi)型溢陪,這種說(shuō)法是相對(duì)于前面的類(lèi)型參數(shù)T
而言的,T
表示類(lèi)型形參睛廊,使用時(shí)被替代為傳入的具體類(lèi)型形真,而?
就是一種具體類(lèi)型,不會(huì)被別的具體類(lèi)型替代超全。
在前面有關(guān)泛型的繼承關(guān)系中咆霜,遇到List<Integer>
不是List<Number>
的子類(lèi)型問(wèn)題,可以使用通配符號(hào)?
表示具體類(lèi)型嘶朱,這樣則可以匹配任意的參數(shù)類(lèi)型蛾坯,示例:
public class Test {
public static void main(String[] args) {
List<?> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
numberList = integerList;
}
}
既然?
可以表示所有類(lèi)型,當(dāng)然也可以表示Integer
類(lèi)型疏遏,所以代碼可以編譯通過(guò)脉课。
在平常的使用中,類(lèi)型的選擇范圍并非如此隨意财异,更多時(shí)候在定義泛型類(lèi)倘零、接口或方法時(shí),限定了能夠使用的類(lèi)型范圍戳寸。
1 限定上界
使用extends
關(guān)鍵字限定參數(shù)類(lèi)型能夠選擇的上界呈驶,示例:
public class Test {
public static void main(String[] args) {
GenericsClass<Integer> integerObj = new GenericsClass<>();
// GenericsClass<String> stringObj = new GenericsClass<>(); compile error
Test.genericsMethod1(new ArrayList<Integer>());
// Test.genericsMethod1(new ArrayList<String>()); compile error
Test.genericsMethod2(new ArrayList<Integer>());
// Test.genericsMethod2(new ArrayList<String>()); compile error
}
static class GenericsClass<T extends Number>{
//...
}
static <T extends Number> void genericsMethod1(List<T> list) {
// list.add(1); compile error
}
static void genericsMethod2(List<? extends Number> list) {
// list.add(1); compile error
}
}
GenericsClass
類(lèi)中通過(guò)<T extends Number>
限定參數(shù)類(lèi)型為Number
的子類(lèi)型,genericsMethod1疫鹊、genericsMethod2
同樣使用extends
關(guān)鍵字限定類(lèi)型上界袖瞻。
genericsMethod1
與genericsMethod2
分別使用了T
和?
作為參數(shù)類(lèi)型符號(hào),在限定類(lèi)型范圍上拆吆,兩者作用相同虏辫。不同之外在于,使用T
表示類(lèi)型形參锈拨,在genericsMethod1
方法體內(nèi)可以引用T
類(lèi)型相關(guān)的操作砌庄,但是?
則無(wú)法引用。
這里需要注意一點(diǎn)奕枢,若使用具有上界的泛型來(lái)作為集合的元素類(lèi)型時(shí)娄昆,因?yàn)榇藭r(shí)無(wú)法確定集合的元素類(lèi)型,所以無(wú)法向集合中添加元素缝彬,示例:
static <T extends Number> void genericsMethod1(List<T> list) {
// list.add(1); compile error
}
static void genericsMethod2(List<? extends Number> list) {
// list.add(1); compile error
}
2 限定下界
使用super
關(guān)鍵字限定參數(shù)類(lèi)型能夠選擇的下界萌焰,示例:
public class Test {
public static void main(String[] args) {
Test.genericsMethod2(new ArrayList<Integer>());
// Test.genericsMethod2(new ArrayList<String>()); compile error
}
// static class GenericsClass<? super Integer>{ compile error
// //...
// }
// static <T super Integer> void genericsMethod1(List<T> list) { compile error
// //...
// }
static void genericsMethod2(List<? super Integer> list) {
list.add(1);
}
}
由示例可知,<? super Integer>
的形式限定元素的下界為Integer
類(lèi)型谷浅,則此時(shí)可以對(duì)集合進(jìn)行添加Integer
元素操作扒俯。
由示例同樣可知奶卓,使用super
關(guān)鍵字限定參數(shù)類(lèi)型下界,與使用extends
關(guān)鍵字限定參數(shù)類(lèi)型的上界有所不同撼玄,最大的區(qū)別就是:類(lèi)型形參T
不能與super
關(guān)鍵字配合使用夺姑。若可以配合使用,則會(huì)存在以下問(wèn)題:
<T extends Integer>
表示T
類(lèi)為Integer
的子類(lèi)型掌猛,則T
類(lèi)型屬性可以訪(fǎng)問(wèn)Integer
類(lèi)型中的部分屬性盏浙;<T super Integer>
的描述表示T
類(lèi)為Integer
的父類(lèi),則T
類(lèi)型屬性不確定其父類(lèi)為何類(lèi)荔茬,也可能為Serializable
废膘,那么此時(shí)將不具備任何屬性,因?yàn)椴淮_定慕蔚,所以無(wú)法進(jìn)行操作丐黄;<T extends Integer>
在編譯時(shí)進(jìn)行類(lèi)型擦除后,則T
屬性將默認(rèn)為extends
繼承的父類(lèi)中最左邊一個(gè)孔飒,這里即為Integer
孵稽;而<T super Integer>
描述的類(lèi),在進(jìn)行類(lèi)型擦除后將無(wú)法確定其類(lèi)型十偶。
根據(jù)以上兩點(diǎn)菩鲜,在類(lèi)的描述中,不能使用<T super Integer>
的形式限定參數(shù)類(lèi)型的下界惦积。
通配符的上下界使用有
PECS(producer extends, consumer super)
原則接校,producer
可以根據(jù)上界進(jìn)行元素讀取,但是不確定類(lèi)型狮崩,所以無(wú)法添加元素蛛勉;consumer
可以根據(jù)下界進(jìn)行元素添加,但是不確定類(lèi)型睦柴,所以無(wú)法讀取元素诽凌。
泛型數(shù)組
在普通數(shù)組的使用中,存在如下的情況:
public class Test {
public static void main(String[] args) {
Integer[] integers = new Integer[5];
Object[] objects = integers;
objects[0] = "abc";
}
}
這段代碼在編譯期是沒(méi)問(wèn)題的坦敌,在運(yùn)行時(shí)會(huì)報(bào)出ArrayStoreException
異常侣诵。這種情況稱(chēng)之為數(shù)組的協(xié)變(covariant)
,即S
類(lèi)型為T
類(lèi)型的子類(lèi)型狱窘,則S
類(lèi)型數(shù)組為T
類(lèi)型數(shù)組的子類(lèi)型杜顺。
為了避免這種協(xié)變的情況發(fā)生,Java
禁止創(chuàng)建具體類(lèi)型的泛型數(shù)組蘸炸,否則對(duì)于泛型數(shù)組有如下情況躬络,示例來(lái)源Java 指導(dǎo)手冊(cè):
// Not really allowed.
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Unsound, but passes run time store check
oa[1] = li;
// Run-time error: ClassCastException.
String s = lsa[1].get(0);
如果Java
中允許創(chuàng)建具體類(lèi)型的泛型數(shù)組,則以上代碼在編譯期通過(guò)類(lèi)型檢查搭儒,在運(yùn)行期獲取元素時(shí)會(huì)報(bào)出ClassCastException
異常穷当,即無(wú)法通過(guò)泛型元素的隱式類(lèi)型轉(zhuǎn)換提茁。
Java
雖然禁止創(chuàng)建具體類(lèi)型的泛型數(shù)組,但并不禁止創(chuàng)建通配符形式的數(shù)組馁菜,如下所示茴扁,示例來(lái)源Java 指導(dǎo)手冊(cè):
// OK, array of unbounded wildcard type.
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0);
雖然發(fā)生運(yùn)行期錯(cuò)誤,但是因?yàn)橥ㄅ浞氖褂没鸬耍栽讷@取元素時(shí)丹弱,需要進(jìn)行顯示類(lèi)型轉(zhuǎn)換德撬,也就是將元素的類(lèi)型操作交給開(kāi)發(fā)人員進(jìn)行控制铲咨。
參考
Type Parameters
Difference between <? super T> and <? extends T> in Java
The Java? Tutorials