全文大約【4600】字,不說(shuō)廢話(huà),只講可以讓你學(xué)到技術(shù)、明白原理的純干貨!本文帶有豐富的案例及配圖視頻,讓你更好地理解和運(yùn)用文中的技術(shù)概念,并可以給你帶來(lái)具有足夠啟迪的思考......
一. 泛型方法
1.簡(jiǎn)介
我們可以在定義接口和類(lèi)時(shí)使用泛型,這樣該接口和類(lèi)中的所有方法及成員變量等處,也都可以使用該泛型。但其實(shí)泛型可以應(yīng)用在整個(gè)類(lèi)上,也可以只應(yīng)用在類(lèi)中的某個(gè)方法上。也就是說(shuō),方法所在的類(lèi)可以是泛型類(lèi),也可以不是泛型類(lèi)。方法中是否帶有泛型,與其所在的類(lèi)有沒(méi)有泛型沒(méi)有關(guān)系。
泛型方法是在調(diào)用方法時(shí)才確定類(lèi)型的方法,泛型可以使得該方法獨(dú)立于類(lèi)而產(chǎn)生變化。另外,static靜態(tài)方法無(wú)法訪問(wèn)泛型類(lèi)的類(lèi)型參數(shù),因此,如果想讓一個(gè)static方法具有泛型能力,就必須使該靜態(tài)方法成為泛型方法。
2.語(yǔ)法
我們?cè)诙x泛型方法時(shí),需要在方法名的前面添加類(lèi)型參數(shù)。定義泛型方法的語(yǔ)法格式如下:
[訪問(wèn)權(quán)限修飾符] [static] [final] <類(lèi)型參數(shù)列表> 返回值類(lèi)型 方法名([形式參數(shù)列表])
例如:
public static <T> List showInfo(Class<T> clazz,int userId){}
一般情況下,我們編寫(xiě)泛型方法時(shí),必須在該方法的名稱(chēng)前聲明要使用的泛型,并且可以同時(shí)聲明多個(gè)泛型,中間也是用逗號(hào)分割。接下來(lái)小編就定義一個(gè)泛型方法,給大家具體介紹一下泛型方法的創(chuàng)建和使用。
3. 代碼案例
這里我們定義一個(gè)泛型方法,用于對(duì)數(shù)組排序后再遍歷元素輸出,代碼如下:
import java.util.Arrays;
/**
* @author 一一哥Sun
*/
public class Demo04 {
//定義了一個(gè)靜態(tài)的泛型方法,遍歷數(shù)組中的每個(gè)元素
public static <T> void printArray(T[] arr) {
//先對(duì)數(shù)組進(jìn)行排序
Arrays.sort(arr);
//再遍歷數(shù)組元素
for (T t : arr) {
System.out.print(t + " ");
}
System.out.println();
}
public static void main(String[] args) {
Integer[] nums= {100,39,8,200,65};
//調(diào)用泛型方法
printArray(nums);
}
}
在上面的代碼中,printArray()就是一個(gè)泛型方法,該方法中使用了類(lèi)型參數(shù)T。并且我們?cè)诜椒ǖ膮?shù)中,使用類(lèi)型參數(shù)T定義了一個(gè)泛型數(shù)組T[],接著對(duì)該數(shù)組進(jìn)行排序和遍歷。這樣以后無(wú)論我們傳入任何類(lèi)型的數(shù)組,都可以在不進(jìn)行類(lèi)型轉(zhuǎn)換的前提下,輕松實(shí)現(xiàn)排序等功能了,這樣我們之前提的需求也就很容易實(shí)現(xiàn)了。
二. 通配符
除了以上這些用法之外,泛型中還有一個(gè)很重要的通配符功能,接下來(lái)我們就來(lái)看看它是怎么回事。
1.簡(jiǎn)介
泛型中的通配符其實(shí)也是一種特殊的泛型類(lèi)型,也稱(chēng)為通配符類(lèi)型參數(shù)。利用通配符類(lèi)型參數(shù),可以讓我們編寫(xiě)出更通用的代碼,甚至可以在不知道實(shí)際類(lèi)型的情況下使用它們。我們一般是使用 ? 來(lái)代替具體的類(lèi)型參數(shù),例如 List 在邏輯上可以等同于 List、List 等所有 List<具體類(lèi)型實(shí)參> 的類(lèi)。
對(duì)此,有的小伙伴可能會(huì)很好奇,我們?yōu)槭裁葱枰ㄅ浞?其實(shí)之所以會(huì)出現(xiàn)通配符,主要是在開(kāi)發(fā)時(shí),有時(shí)候我們需要一個(gè)泛型類(lèi)型,但我們卻不知道該使用哪個(gè)具體的類(lèi)型。在這種情況下,我們就可以使用通配符類(lèi)型參數(shù),讓代碼更加地通用。比如,我們想編寫(xiě)一個(gè)可以接受任何類(lèi)型的集合,并返回其中最大的元素時(shí),此時(shí)我們可能并不確定到底該傳入哪個(gè)具體的集合,那使用通配符就會(huì)更好一些。
2.通配符的形式
泛型通配符在具體使用時(shí),有如下三種實(shí)現(xiàn)形式:
● 未限定通配符(?):?表示未知類(lèi)型的通配符;
● 上限通配符(? extends T):?表示類(lèi)型上限的通配符,T是一個(gè)類(lèi)或接口;
● 下限通配符(? super T):?表示類(lèi)型下限的通配符,T是一個(gè)類(lèi)或接口。
接下來(lái)小編針對(duì)以上這三種形式,分別通過(guò)幾個(gè)案例來(lái)給大家講解其用法。
3.未限定通配符(?)
未限定通配符(?)是一種表示未知類(lèi)型的通配符,它可以在需要一個(gè)類(lèi)型參數(shù)的情況下使用。但由于沒(méi)有限制,因此它只能用于簡(jiǎn)單的情況,例如集合中的迭代器或者返回類(lèi)型是泛型的方法等。下面是一個(gè)簡(jiǎn)單的例子:
import java.util.ArrayList;
import java.util.List;
/**
* @author 一一哥Sun
*/
public class Demo05 {
public static void main(String[] args) {
List<String> names = new ArrayList<String>();
List<Integer> ages = new ArrayList<Integer>();
List<Number> numbers = new ArrayList<Number>();
names.add("一一哥");
names.add("秦始皇");
ages.add(28);
ages.add(50);
ages.add(28);
numbers.add(100);
numbers.add(800);
printElement(names);
printElement(ages);
printElement(numbers);
}
//未限定通配符的使用
public static void printElement(List<?> data) {
for(int i=0;i<data.size();i++) {
//data.getClass().getSimpleName():用于獲取某個(gè)類(lèi)的類(lèi)名
System.out.println(data.getClass().getSimpleName()+"--data: " + data.get(i));
}
}
}
在這個(gè)例子中,printElement()方法就接受了一個(gè)未知類(lèi)型的List集合,所以names,ages,numbers都可以作為這個(gè)方法的實(shí)參,這就是未限定通配符的作用。
4. PECS原則
PECS是Producer Extends Consumer Super的縮寫(xiě),這是關(guān)于Java泛型的一種設(shè)計(jì)原則。該原則表示,如果我們需要返回T,它是生產(chǎn)者(Producer),要使用extends通配符;如果需要寫(xiě)入T,它就是消費(fèi)者(Consumer),要使用super通配符。該原則可以指導(dǎo)我們?cè)谑褂梅盒蜁r(shí),該如何定義類(lèi)型參數(shù)的上限和下限。
當(dāng)我們使用泛型時(shí),可能需要定義類(lèi)型參數(shù)的上限和下限。例如,我們想要編寫(xiě)一個(gè)方法來(lái)處理一些集合類(lèi)型,但我們不知道這些集合中到底有什么類(lèi)型的元素,此時(shí)我們就可以定義一個(gè)類(lèi)型參數(shù)來(lái)處理所有的集合類(lèi)型。一般我們可以利用extends來(lái)設(shè)置泛型上限,利用super來(lái)設(shè)置泛型下限。接下來(lái)小編會(huì)在下面的第5和第6小結(jié)中,給大家講解泛型的上限和下限具體該如何實(shí)現(xiàn),請(qǐng)大家繼續(xù)往下學(xué)習(xí)。
5. 上限通配符(? extends T)
上限通配符(?extends T)是一種表示類(lèi)型上限的通配符,其中T是一個(gè)類(lèi)或接口,泛型類(lèi)的類(lèi)型必須實(shí)現(xiàn)或繼承 T這個(gè)接口或類(lèi)。它指定了可以使用的類(lèi)型上限,主要是用于限制輸入的參數(shù)類(lèi)型。
import java.util.ArrayList;
import java.util.List;
/**
* @author 一一哥Sun
*/
public class Demo06 {
public static void main(String[] args) {
List<String> names = new ArrayList<String>();
List<Integer> ages = new ArrayList<Integer>();
List<Number> numbers = new ArrayList<Number>();
names.add("一一哥");
names.add("秦始皇");
ages.add(28);
ages.add(50);
ages.add(28);
numbers.add(100);
numbers.add(800);
//String等非Number類(lèi)型就不行
//printElementUpbound(names);
//泛型值只能是Number及其子類(lèi)類(lèi)型,所以Integer/Double/Float等類(lèi)型都可以,但String就不行
printElementUpbound(ages);
printElementUpbound(numbers);
}
//上限通配符的使用,這里的泛型值只能是Number及其子類(lèi)類(lèi)型
public static void printElementUpbound(List<? extends Number> data) {
for(int i=0;i<data.size();i++) {
//data.getClass().getSimpleName():用于獲取某個(gè)類(lèi)的類(lèi)名
System.out.println(data.getClass().getSimpleName()+"--data: " + data.get(i));
}
}
}
在這個(gè)例子中,printElementUpbound方法中的集合泛型,可以是Number類(lèi)或其子類(lèi),除此之外的其他類(lèi)型都不行。也就是說(shuō),我們只能使用Number或其子類(lèi)作為類(lèi)型參數(shù),泛型類(lèi)型的上限是Number,這就是上限通配符的含義。
6. 下限通配符(? super T)
下限通配符(?super T)是一種表示類(lèi)型下限的通配符,其中T是一個(gè)類(lèi)或接口。它指定了可以使用的類(lèi)型下限,主要用于限制輸出的參數(shù)類(lèi)型。下面是一個(gè)簡(jiǎn)單的例子:
import java.util.ArrayList;
import java.util.List;
/**
* @author 一一哥Sun
* V我領(lǐng)資料:syc_2312119590
* 各平臺(tái)都有小編的同名博客哦
*/
public class Demo07 {
public static void main(String[] args) {
List<String> names = new ArrayList<String>();
List<Integer> ages = new ArrayList<Integer>();
List<Double> numbers = new ArrayList<Double>();
names.add("一一哥");
names.add("秦始皇");
ages.add(28);
ages.add(50);
ages.add(28);
numbers.add(100.0);
numbers.add(800.9);
//String等非Number類(lèi)型就不行
//printElementUpbound(names);
//此時(shí)Double類(lèi)型也不行
//printElementDownbound(numbers);
//泛型值只能是Integer及其父類(lèi)類(lèi)型,所以Double/Float/String等類(lèi)型都不可以
printElementDownbound(ages);
}
//下限通配符的使用,這里的泛型值只能是Integer及其父類(lèi)類(lèi)型
public static void printElementDownbound(List<? super Integer> data) {
for(int i=0;i<data.size();i++) {
System.out.println(data.getClass().getSimpleName()+"--data: " + data.get(i));
}
}
}
在這個(gè)例子中,printElementDownbound方法中的集合泛型,可以是Integer或其父類(lèi)型,即類(lèi)型下限是Integer,除此之外的其他類(lèi)型都不行。也就是說(shuō),我們只能使用Integer或其父類(lèi)作為類(lèi)型參數(shù),泛型類(lèi)型的下限是Integer,這就是下限通配符的含義。
7. 和的區(qū)別
在這里,小編要給大家再總結(jié)一下和的區(qū)別:
● 允許調(diào)用T get()這樣的讀方法來(lái)獲取T對(duì)象的引用,但不允許調(diào)用set(T)這樣的寫(xiě)方法來(lái)傳入T的引用(傳入null除外);
● 允許調(diào)用set(T)這樣的寫(xiě)方法傳入T對(duì)象的引用,但不允許調(diào)用T get()這樣的讀方法來(lái)獲取T對(duì)象的引用(獲取Object除外)。
● 即允許讀不允許寫(xiě),允許寫(xiě)不允許讀。
大家注意,無(wú)論是未限定通配符、上限通配符還是下限通配符,我們既可以在方法中使用,也可以在類(lèi)或接口中使用。
三. 泛型擦除
我們?cè)趯W(xué)習(xí)泛型時(shí),除了要掌握上面這些泛型類(lèi)、泛型接口、泛型方法以及通配符等內(nèi)容之外,還要學(xué)習(xí)泛型擦除的相關(guān)內(nèi)容。那么什么是泛型擦除呢?我們繼續(xù)往下學(xué)習(xí)吧。
1.簡(jiǎn)介
所謂的泛型擦除(Type Erasure),就是指在編譯時(shí),JVM編譯器會(huì)將所有的泛型信息都擦除掉,變成原始類(lèi)型,一般是將泛型的類(lèi)型參數(shù)替換成具體類(lèi)型的上限或下限(如果沒(méi)有指定上界,則默認(rèn)為Object)。
換句話(huà)說(shuō),雖然我們?cè)诖a中使用了泛型,但在編譯后,所有的泛型類(lèi)型都會(huì)被擦除掉,轉(zhuǎn)而使用其對(duì)應(yīng)的原始類(lèi)型。這就是Java泛型的底層實(shí)現(xiàn)原理。這樣設(shè)計(jì)的目的是為了兼容舊的JDK版本,使得Java具有了較好的向后兼容性,舊的非泛型代碼可以直接使用泛型類(lèi)庫(kù),而不需要進(jìn)行任何修改。同時(shí),Java也提供了反射機(jī)制來(lái)操作泛型類(lèi)型,使得泛型類(lèi)型在某些情況下還是可以被獲取到的,所以即使有泛型擦除,仍然也不會(huì)太影響Java虛擬機(jī)的運(yùn)行時(shí)效率。
比如,在我們定義一個(gè)泛型類(lèi)時(shí),我們會(huì)使用泛型類(lèi)型參數(shù)來(lái)代替具體的類(lèi)型,好比下面這個(gè)例子:
public class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
}
2.泛型擦除帶來(lái)的限制
在編譯之后,這個(gè)泛型類(lèi)的類(lèi)型參數(shù)T就會(huì)被擦除,成為其對(duì)應(yīng)的原始類(lèi)型Object。這也意味著,我們無(wú)法在運(yùn)行時(shí)獲取到泛型的實(shí)際類(lèi)型參數(shù),所以泛型擦除的使用會(huì)有一些限制。首先由于泛型類(lèi)型參數(shù)被擦除了,因此我們?cè)谶\(yùn)行時(shí)就無(wú)法獲得泛型類(lèi)型參數(shù)的信息。例如,如果我們有一個(gè)List類(lèi)型的變量,在運(yùn)行時(shí)我們就無(wú)法獲得這個(gè)List集合中的元素類(lèi)型是Integer。另一個(gè)限制是在使用泛型類(lèi)型時(shí),還需要注意類(lèi)型安全性。在編譯階段,編譯器會(huì)檢查泛型類(lèi)型的類(lèi)型安全性;但在運(yùn)行階段,由于泛型類(lèi)型參數(shù)被擦除了,因此就無(wú)法保證類(lèi)型安全性了。泛型擦除的限制,主要表現(xiàn)在以下幾個(gè)方面:
無(wú)法使用基本類(lèi)型實(shí)例化類(lèi)型參數(shù);
無(wú)法在運(yùn)行時(shí)獲取泛型類(lèi)型信息;
泛型類(lèi)型參數(shù)不能用于靜態(tài)變量或靜態(tài)方法;
不能實(shí)例化T類(lèi)型。
接下來(lái)小編再給大家具體分析一下這些限制。
2.1 無(wú)法使用基本類(lèi)型實(shí)例化類(lèi)型參數(shù)
Java泛型中的類(lèi)型參數(shù)不能是基本類(lèi)型,只能是類(lèi)或接口類(lèi)型。例如,以下代碼在編譯階段會(huì)出錯(cuò),無(wú)法通過(guò)編譯:
List<int> list = new ArrayList<int>();
正確的寫(xiě)法是使用基本類(lèi)型對(duì)應(yīng)的包裝類(lèi)型,如下所示:
List<Integer> list = new ArrayList<Integer>();
2.2 無(wú)法在運(yùn)行時(shí)獲取泛型類(lèi)型信息
由于泛型擦除的存在,導(dǎo)致我們?cè)诔绦蜻\(yùn)行時(shí)無(wú)法獲取泛型類(lèi)型的信息。例如,以下代碼在運(yùn)行時(shí)就無(wú)法獲取List的元素類(lèi)型:
List<String> list = new ArrayList<String>();
Class<?> clazz = list.getClass();
Type type = clazz.getGenericSuperclass();
// 輸出:class java.util.ArrayList<E>
System.out.println(type);
在輸出的結(jié)果中,我們只能得到ArrayList的類(lèi)型信息,而無(wú)法獲取到集合中具體的泛型類(lèi)型信息,也就是獲取不到String的信息。但如果我們就是想在運(yùn)行時(shí)獲取到泛型的實(shí)際類(lèi)型參數(shù),其實(shí)可以參考以下方式進(jìn)行實(shí)現(xiàn):
public class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
public Class<?> getContentType() {
return content.getClass();
}
}
在上面的代碼中,我們新增了一個(gè)方法 getContentType(),該方法用于返回泛型類(lèi)型參數(shù)的實(shí)際字節(jié)碼類(lèi)型,以后我們通過(guò)調(diào)用這個(gè)方法,就可以間接地獲取到泛型類(lèi)型的信息了。
2.3 泛型類(lèi)型參數(shù)不能用于靜態(tài)變量或靜態(tài)方法
由于泛型類(lèi)型參數(shù)是在實(shí)例化對(duì)象時(shí)才被確定的,因此不能在靜態(tài)變量或靜態(tài)方法中使用泛型類(lèi)型參數(shù)。例如,以下代碼是無(wú)法編譯通過(guò)的:
public class MyClass<T> {
//這樣的代碼編譯不通過(guò)
private static T value;
public static void setValue(T value) {
MyClass.value = value;
}
}
正確的寫(xiě)法是使用一個(gè)實(shí)際類(lèi)型來(lái)代替泛型類(lèi)型參數(shù):
public class MyClass {
private static String value;
public static void setValue(String value) {
MyClass.value = value;
}
}
2.4 不能實(shí)例化T類(lèi)型
比如在下面的案例中:
public class MyClass<T> {
private T first;
private T last;
public MyClass() {
first = new T();
last = new T();
}
}
上述代碼無(wú)法通過(guò)編譯,因?yàn)闃?gòu)造方法的兩行語(yǔ)句:
first = new T();
last = new T();
擦拭后實(shí)際上變成了:
first = new Object();
last = new Object();
這樣一來(lái),創(chuàng)建new MyClass()和創(chuàng)建new MyClass()就變成了Object,編譯器會(huì)阻止這種類(lèi)型不對(duì)的代碼。如果我們想對(duì)泛型T進(jìn)行實(shí)例化,需要借助Class參數(shù)并集合反射技術(shù)來(lái)實(shí)現(xiàn),且在使用時(shí)也必須傳入Class。
四. 結(jié)語(yǔ)
不過(guò),盡管泛型擦除有一些限制,但泛型仍然不失為一種強(qiáng)大的編程工具,它可以提高代碼的可讀性和可維護(hù)性。通過(guò)合理地使用泛型,我們可以在編譯時(shí)進(jìn)行類(lèi)型檢查,避免類(lèi)型轉(zhuǎn)換的錯(cuò)誤和運(yùn)行時(shí)異常,從而提高了代碼的安全性和可靠性。同時(shí),我們也需要了解Java泛型擦除的限制,以便在實(shí)際應(yīng)用中做出正確的決策。