什么是泛型?
泛型是程序設(shè)計語言的一種特性。允許程序員在強(qiáng)類型程序設(shè)計語言中編寫代碼時定義一些可變部分,那些部分在使用前必須作出指明。各種程序設(shè)計語言和其編譯器、運(yùn)行環(huán)境對泛型的支持均不一樣。將類型參數(shù)化以達(dá)到代碼復(fù)用提高軟件開發(fā)工作效率的一種數(shù)據(jù)類型。泛型類是引用類型,是堆對象,主要是引入了類型參數(shù)這個概念。
泛型的用處?
一些強(qiáng)類型編程語言支持泛型,其主要目的是加強(qiáng)類型安全及減少類轉(zhuǎn)換的次數(shù),但一些支持泛型的編程語言只能達(dá)到部分目的。
Java早期是使用Object類型來接收任意的對象類型。但是在實(shí)際的運(yùn)用中會有類型轉(zhuǎn)換的問題,即:向上轉(zhuǎn)型沒有任何問題,但是向下轉(zhuǎn)型時其實(shí)隱含了類型轉(zhuǎn)換的問題,這就是程序不安全的原因,也就是因?yàn)榇嬖谶@種安全隱患,所以Java在JDK5以后就提供了泛型來解決這個安全問題。
使用泛型定義的可以防止在運(yùn)行時強(qiáng)轉(zhuǎn)類型導(dǎo)致出現(xiàn)ClassCastExecption,把所有的因?yàn)轭愋筒幻鞔_的問題都在編譯時指出。
有了泛型以后:
1、代碼更加簡潔【不用強(qiáng)制轉(zhuǎn)換】
2、程序更加健壯【只要編譯時期沒有警告,那么運(yùn)行時期就不會出現(xiàn)ClassCastException異?!?br>
3、可讀性和穩(wěn)定性更強(qiáng)【在編寫集合的時候,就限定了類型】
泛型的分類
泛型類
- 泛型類就是把泛型定義在類上,創(chuàng)建類或使用類時,需要提前把類型明確,這樣的話提前明確了類型表示該類就是什么類型。不用擔(dān)心強(qiáng)轉(zhuǎn)或者運(yùn)行時類型不安全問題。
泛型方法
- 定義在方法上表示使用方法時傳入或者取出的參數(shù)類型需要明確。
泛型接口
- 說白了就是泛型類,因?yàn)榻涌谛枰悂韺?shí)現(xiàn)才能使用。
泛型類派生的子類
- 分為明確類型的子類和不明確類型的子類。
不明確類型的就需要在下一層的子類或者創(chuàng)建的實(shí)例類來明確類型 - 注意
重寫父類方法,返回值的類型要和父類一樣
類聲明的泛型只對非靜態(tài)成員有效
因?yàn)殪o態(tài)成員在dex裝載時就會把所有靜態(tài)修飾的字段全部初始化了,并不是運(yùn)行時再去明確類型。
類型通配符
通配符以 ?修飾的,表示未知類型。
通配符可用作各種情況:作為參數(shù),字段或局部變量的類型;有時也作為返回類型;通配符從不用作泛型方法調(diào)用、泛型類實(shí)例創(chuàng)建或超類型的類型參數(shù)。
為什么要用通配符?
例子:你要遍歷并打印外界傳入的一個List,但是你不知道這個List的類型是什么?你可以使用Object修飾泛型,但是泛型中的<Object>并不是像以前那樣有繼承關(guān)系的,也就是說List<Object>和List<String>是毫無關(guān)系的!我們只能遍歷裝載object的集合。
所以Java泛型提供了類型通配符
?號通配符可以表示可以匹配任意類型,但是使用?通配符只能調(diào)用和類型無關(guān)的方法,不能調(diào)用于類型有關(guān)的方法。也就是說,在上面的List集合,我是不能使用add()方法的。因?yàn)閍dd()方法是把對象丟進(jìn)集合中,而現(xiàn)在我是不知道對象的類型是什么。
private void demo(List<?> list){
list.get(index);//這是沒問題的
list.add(任意元素);//提示:in List cannot be applied to XX類型
}
上界通配符
表示設(shè)定類型的上限,也就是說是明確他的類型最多是定義的類型自身和它的子類
例子:現(xiàn)在,我想接收一個List集合,它只能操作數(shù)字類型的元素【Float、Integer、Double、Byte等數(shù)字類型都行】,怎么做???
List<? extends Number>
這樣你就把元素類型給明確了,List裝載的對象只能是Number的子類或自身。
下界通配符
表示設(shè)定類型的下限,表示設(shè)定的類型只能是它自身或者它的父類
例子:
<? super Type>
那它有什么用呢??我們來想一下,當(dāng)我們想要創(chuàng)建一個TreeSet<String>類型的變量的時候,并傳入一個可以比較String大小的Comparator。
那么這個Comparator的選擇就有很多了,它可以是Comparator<String>,還可以是類型參數(shù)是String的父類,比如說Comparator<Objcet>....
這樣做,就非常靈活了。也就是說,只要它能夠比較字符串大小,就行了
PECS原則
如果從集合中讀取類型T的元素,但是不能寫入,可以使用? extends 通配符
如果從集合中寫入類型T的元素,但是不能讀取,可以使用? super 通配符
如果既要讀也要寫,那么就不要使用任何通配符
例子:
? extends T
public class Test {
static class Food {}
static class Fruit extends Food {}
static class Apple extends Fruit {}
public static void main(String[] args) throws IOException {
List<? extends Fruit> fruits = new ArrayList<>();
fruits.add(new Food()); // compile error
fruits.add(new Fruit()); // compile error
fruits.add(new Apple()); // compile error
fruits = new ArrayList<Fruit>(); // compile success
fruits = new ArrayList<Apple>(); // compile success
fruits = new ArrayList<Food>(); // compile error
fruits = new ArrayList<? extends Fruit>(); // compile error: 通配符類型無法實(shí)例化
Fruit object = fruits.get(0); // compile success
}
}
存入數(shù)據(jù)
- 賦值是參數(shù)化類型為 Fruit 的集合和其子類的集合都可以成功,通配符類型無法實(shí)例化。
- 編譯器會阻止將任何類加入fruits。在向fruits中添加元素時,編譯器會檢查類型是否符合要求。因?yàn)榫幾g器只知道fruits是Fruit某個子類的List,但并不知道這個子類具體是什么類,為了類型安全,只好阻止向其中加入任何子類。
- 那么可不可以加入Fruit呢?很遺憾,也不可以。事實(shí)上,不能往使用了? extends的數(shù)據(jù)結(jié)構(gòu)里寫入任何的值。
讀取數(shù)據(jù)
但是,由于編譯器知道它總是Fruit的子類型,因此我們總可以從中讀取出Fruit對象:
Fruit fruit = fruits.get(0);
? super T
public class Test {
static class Food {}
static class Fruit extends Food {}
static class Apple extends Fruit {}
public static void main(String[] args) throws IOException {
List<? super Fruit> fruits = new ArrayList<>();
fruits.add(new Food()); // compile error
fruits.add(new Fruit()); // compile success
fruits.add(new Apple()); // compile success
fruits = new ArrayList<Fruit>(); // compile success
fruits = new ArrayList<Apple>(); // compile error
fruits = new ArrayList<Food>(); // compile success
fruits = new ArrayList<? super Fruit>(); // compile error: 通配符類型無法實(shí)例化
Fruit object = fruits.get(0); // compile error
}
}
存入數(shù)據(jù)
- super 通配符類型同樣不能實(shí)例化,F(xiàn)ruit 和其超類的集合均可賦值
- 這里 add 時 Fruit 及其子類均可成功,為啥呢?因?yàn)橐阎?fruits 的參數(shù)化類型必定是 Fruit 或其超類 T,那么 Fruit 及其子類肯定可以賦值給 T。
- 出于對類型安全的考慮,我們可以加入Apple對象或者其任何子類(如RedApple)對象(因?yàn)榫幾g器會自動向上轉(zhuǎn)型),但由于編譯器并不知道List的內(nèi)容究竟是Apple的哪個超類,因此不允許加入特定的任何超類型。
讀取數(shù)據(jù)
編譯器在不知道這個超類具體是什么類,只能返回Object對象,因?yàn)镺bject是任何Java類的最終祖先類。
Object fruit = fruits.get(0);
泛型方法和類型通配符的區(qū)別?
例子:
a. 類型通配符:void func(List<? extends A> list);
b. 完全可以用泛型方法完美解決:<T extends A> void func(List<T> list);
1)你會發(fā)現(xiàn)所有能用類型通配符(?)解決的問題都能用泛型方法解決,并且泛型方法可以解決的更好。
上面兩種方法可以達(dá)到相同的效果(?可以代表范圍內(nèi)任意類型,而T也可以傳入范圍內(nèi)的任意類型實(shí)參),并且泛型方法更進(jìn)一步,?泛型對象是只讀的,而泛型方法里的泛型對象是可修改的,即List<T> list中的list是可修改的??!
- 要說兩者最明顯的區(qū)別就是:
- ?泛型對象是只讀的,不可修改,因?yàn)?類型是不確定的,可以代表范圍內(nèi)任意類型;
- 而泛型方法中的泛型參數(shù)對象是可修改的,因?yàn)轭愋蛥?shù)T是確定的(在調(diào)用方法時確定),因?yàn)門可以用范圍內(nèi)任意類型指定;
注意:前者是代表,后者是指定,指定就是確定的意思,而代表卻不知道代表誰,可以代表范圍內(nèi)所有類型。
- 這樣好像說的通配符?一無是處,但是并不是這樣,Java設(shè)計類型通配符?是有道理的,首先一個最明顯的優(yōu)點(diǎn)就是?的書寫要比泛型方法簡潔,無需先聲明類型參數(shù),其次它們有各自的應(yīng)用場景:
- 一般只讀就用?,要修改就用泛型方法,例如一個進(jìn)行修改的典型的泛型方法的
例子:
public <T> void func(List<T> list, T t) {
list.add(t);
}
- 在多個參數(shù)、返回值之間存在類型依賴關(guān)系就應(yīng)該使用泛型方法,否則就應(yīng)該是通配符?:
具體講就是,如果一個方法的返回值、某些參數(shù)的類型依賴另一個參數(shù)的類型就應(yīng)該使用泛型方法,因?yàn)楸灰蕾嚨念愋腿绻遣淮_定的?,那么其他元素就無法依賴它)。
例如:
<T> void func(List<? extends T> list, T t);
即第一個參數(shù)依賴第二個參數(shù)的類型(第一個參數(shù)list的類型參數(shù)必須是第二個參數(shù)的類型或者其子類);
可以看到,Java支持泛型方法和?混用;
這個方法也可以寫成:
<T, E extends T> void func(List<E> list, T t);
明顯意義是一樣的,只不過這個list可以修改,而上一個list無法修改。
總之就是一旦返回值、形參之間存在類型依賴關(guān)系就只能使用泛型方法;
否則就應(yīng)該使用? ;
- 對泛型方法的類型參數(shù)進(jìn)行規(guī)約:即有時候可能不必使用泛型方法的地方你不小心麻煩地寫成了泛型方法,而此時你可以將其規(guī)約成使用?的最簡形式
- 總結(jié)地來講就是一句話:只出現(xiàn)一次 & 對它沒有任何依賴
例如:
<T, E extends T> void func(List<T> l1, List<E> l2);
這里E只在形參中出現(xiàn)了一次(類型參數(shù)聲明不算),并且沒有任何其他東西(方法形參、返回值)依賴它,那么就可以把E規(guī)約成?
!!最終規(guī)約的結(jié)果就是:
<T> void func(List<T> l1, List<? extends T> l2);
- 一個最典型的應(yīng)用就是容器賦值方法(Java的API):
public static <T> void Collections.copy(List<T> dest, List<? extends T> src) { ... }
!!從src拷貝到dest,那么dest最好是src的類型或者其父類,因?yàn)檫@樣才能類型兼容,并且src只是讀取,沒必要做修改,因此使用?還可以強(qiáng)制避免你對src做不必要的修改,增加的安全性。
原則
- 如果參數(shù)之間的類型有依賴關(guān)系,或者返回值是與參數(shù)之間有依賴關(guān)系的。那么就使用泛型方法。
- 如果沒有依賴關(guān)系的,就使用通配符,通配符會靈活一些。
kotlin 泛型協(xié)變和泛型約束
out T 與 in T
out T 等價于 ? extends T in T 等價于 ? super T 此外, 還有 * 等價于 ?
val list:ArrayList<out Number> = arrayListOf()
val lisat:ArrayList<in Number> = arrayListOf()
泛型約束
//表示T泛型參數(shù)約束為Number和其子類
private fun Demo(T: Number) {
val list: ArrayList<in Number> = arrayListOf()
list.add(T)
}
什么是泛型擦除
JDK5提出了泛型這個概念,但是JDK5以前是沒有泛型的。也就是泛型是需要兼容JDK5以下的集合的。
Java中的泛型都是偽泛型,意思是在Java虛擬機(jī)中不存在泛型類型對象,所有對象都是普通類。
泛型是提供給javac編譯器使用的,它用于限定集合的輸入類型,讓編譯器在源代碼級別上,即擋住向集合中插入非法數(shù)據(jù)。但編譯器編譯完帶有泛形的java程序后,生成的class文件中將不再帶有泛形信息,以此使程序運(yùn)行效率不受到影響,這個過程稱之為“擦除”。
比如限定一個Animals類型,那么在虛擬機(jī)中所存的就是Animals類,不存在其子類。
無論何時定義一個泛型類型,都會自動提供一個相應(yīng)的 原始類型 (raw type)(不存在泛型 )。原始類型的名字就是刪去類型參數(shù)后泛型類的類型名,擦除類型變量,并替換為 限定類型(沒有限定的變量就用 Object )。
類擦除后如何保證類型限定符的類型?
Java 編譯器通過先檢查代碼中泛型的類型,然后再進(jìn)行類型擦除,再進(jìn)行編譯。
翻譯泛型表達(dá)式
當(dāng)程序調(diào)用泛型方法時,因?yàn)樘摂M機(jī)進(jìn)行了泛型擦除,所以這個時候獲取到的類型應(yīng)該是其類型上限,但是在虛擬機(jī)中會進(jìn)行強(qiáng)制轉(zhuǎn)換成對應(yīng)類型。
那么它是怎么知道什么時候轉(zhuǎn)換成哪個類型呢?
看下ArrayList.get()方法
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
可以看到,在return之前,會根據(jù)泛型變量進(jìn)行強(qiáng)轉(zhuǎn)。假設(shè)泛型類型變量為Date,雖然泛型信息會被擦除掉,但是會將(E) elementData[index],編譯為(Date)elementData[index]。所以我們不用自己進(jìn)行強(qiáng)轉(zhuǎn)。當(dāng)存取一個泛型域時也會自動插入強(qiáng)制類型轉(zhuǎn)換。假設(shè)Pair類的value域是public的,那么表達(dá)式:
Date date = pair.value;
類型擦除與多態(tài)的沖突和解決方法
例子:
現(xiàn)在有這樣一個泛型類:
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
然后我們想要一個子類繼承它。
class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
在這個子類中,我們設(shè)定父類的泛型類型為Pair<Date>,在子類中,我們覆蓋了父類的兩個方法,我們的原意是這樣的:將父類的泛型類型限定為Date,那么父類里面的兩個方法的參數(shù)都為Date類型。
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
所以,我們在子類中重寫這兩個方法一點(diǎn)問題也沒有,實(shí)際上,從他們的@Override標(biāo)簽中也可以看到,一點(diǎn)問題也沒有,實(shí)際上是這樣的嗎?
分析:實(shí)際上,類型擦除后,父類的的泛型類型全部變?yōu)榱嗽碱愋蚈bject,所以父類編譯之后會變成下面的樣子:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
再看子類的兩個重寫的方法的類型:
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
先來分析setValue方法,父類的類型是Object,而子類的類型是Date,參數(shù)類型不一樣,這如果實(shí)在普通的繼承關(guān)系中,根本就不會是重寫,而是重載。
為什么會這樣呢?
原因是這樣的,我們傳入父類的泛型類型是Date,Pair<Date>,我們的本意是將泛型類變?yōu)槿缦拢?/p>
class Pair {
private Date value;
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
}
然后再子類中重寫參數(shù)類型為Date的那兩個方法,實(shí)現(xiàn)繼承中的多態(tài)。
可是由于種種原因,虛擬機(jī)并不能將泛型類型變?yōu)镈ate,只能將類型擦除掉,變?yōu)樵碱愋蚈bject。這樣,我們的本意是進(jìn)行重寫,實(shí)現(xiàn)多態(tài)??墒穷愋筒脸?,只能變?yōu)榱酥剌d。這樣,類型擦除就和多態(tài)有了沖突。JVM知道你的本意嗎?知道?。?!可是它能直接實(shí)現(xiàn)嗎,不能!?。∪绻娴牟荒艿脑?,那我們怎么去重寫我們想要的Date類型參數(shù)的方法啊。
于是JVM采用了一個特殊的方法,來完成這項功能,那就是橋方法。
首先,我們用javap -c className的方式反編譯下DateInter子類的字節(jié)碼,結(jié)果如下:
class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
com.tao.test.DateInter();
Code:
0: aload_0
1: invokespecial #8 // Method com/tao/test/Pair."<init>":()V
4: return
public void setValue(java.util.Date); //我們重寫的setValue方法
Code:
0: aload_0
1: aload_1
2: invokespecial #16 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V
5: return
public java.util.Date getValue(); //我們重寫的getValue方法
Code:
0: aload_0
1: invokespecial #23 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object;
4: checkcast #26 // class java/util/Date
7: areturn
public java.lang.Object getValue(); //編譯時由編譯器生成的巧方法
Code:
0: aload_0
1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去調(diào)用我們重寫的getValue方法;
4: areturn
public void setValue(java.lang.Object); //編譯時由編譯器生成的巧方法
Code:
0: aload_0
1: aload_1
2: checkcast #26 // class java/util/Date
5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去調(diào)用我們重寫的setValue方法)V
8: return
}
從編譯的結(jié)果來看,我們本意重寫setValue和getValue方法的子類,竟然有4個方法,其實(shí)不用驚奇,最后的兩個方法,就是編譯器自己生成的橋方法。可以看到橋方法的參數(shù)類型都是Object,也就是說,子類中真正覆蓋父類兩個方法的就是這兩個我們看不到的橋方法。而打在我們自己定義的setvalue和getValue方法上面的@Oveerride只不過是假象。而橋方法的內(nèi)部實(shí)現(xiàn),就只是去調(diào)用我們自己重寫的那兩個方法。
所以,虛擬機(jī)巧妙的使用了橋方法,來解決了類型擦除和多態(tài)的沖突。
不過,要提到一點(diǎn),這里面的setValue和getValue這兩個橋方法的意義又有不同。
setValue方法是為了解決類型擦除與多態(tài)之間的沖突。
而getValue卻有普遍的意義,怎么說呢,如果這是一個普通的繼承關(guān)系:
那么父類的setValue方法如下:
public Object getValue() {
return super.getValue();
}
而子類重寫的方法是:
其實(shí)這在普通的類繼承中也是普遍存在的重寫,這就是協(xié)變。
并且,還有一點(diǎn)也許會有疑問,子類中的橋方法Object getValue()和Date getValue()是同 時存在的,可是如果是常規(guī)的兩個方法,他們的方法簽名是一樣的,也就是說虛擬機(jī)根本不能分別這兩個方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機(jī)卻是允許這樣做的,因?yàn)樘摂M機(jī)通過參數(shù)類型和返回類型來確定一個方法,所以編譯器為了實(shí)現(xiàn)泛型的多態(tài)允許自己做這個看起來“不合法”的事情,然后交給虛擬器去區(qū)別。
泛型類型變量不能是基本數(shù)據(jù)類型
不能用類型參數(shù)替換基本類型。就比如,沒有ArrayList<double>,只有ArrayList<Double>。因?yàn)楫?dāng)類型擦除后,ArrayList的原始類型變?yōu)镺bject,但是Object類型不能存儲double值,只能引用Double的值。
編譯時集合的instanceof
ArrayList<String> arrayList = new ArrayList<String>();
因?yàn)轭愋筒脸?,ArrayList<String>只剩下原始類型,泛型信息String不存在了。
那么,編譯時進(jìn)行類型查詢的時候使用下面的方法是錯誤的
if( arrayList instanceof ArrayList<String>)
泛型在靜態(tài)方法和靜態(tài)類中的問題
泛型類中的靜態(tài)方法和靜態(tài)變量不可以使用泛型類所聲明的泛型類型參數(shù)
public class Test2<T> {
public static T one; //編譯錯誤
public static T show(T one){ //編譯錯誤
return null;
}
}
因?yàn)榉盒皖愔械姆盒蛥?shù)的實(shí)例化是在定義對象的時候指定的,而靜態(tài)變量和靜態(tài)方法不需要使用對象來調(diào)用。對象都沒有創(chuàng)建,如何確定這個泛型參數(shù)是何種類型,所以當(dāng)然是錯誤的。
但是要注意區(qū)分下面的一種情況:
public class Test2<T> {
public static <T>T show(T one){ //這是正確的
return null;
}
}
因?yàn)檫@是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的 T,而不是泛型類中的T。