- tags:泛型
- categories:筆記
- date: 2017-06-10 11:24:24
在java代碼開發(fā)中,泛型的使用是非常普遍的袁稽,特別是在集合中扰付。那么,其實java中的泛型并不是真正意義上的新的類型价脾,只是java語法中的語法糖已。便于java編譯器在編譯源代碼的時候進行類型校驗笛匙,今早發(fā)現(xiàn)程序錯誤而已侨把。但是對于我們程序員來說的話,有了泛型妹孙,確實會對我們程序開發(fā)秋柄,無論是代碼可讀性,還是開發(fā)效率都有一定的積極作用蠢正。所以华匾,就打算來分析分析泛型內容,是怎么來的机隙,有什么內容蜘拉,該如何使用。
泛型Type與Class
下面可以來討論看看泛型-Type接口-Class類型之間的關系有鹿?
泛型出現(xiàn)之前的類型
在JDK1.5之前旭旭,沒有引入泛型類型的概念的時候,只有所謂的原始類型(相對于泛型參數(shù)化的List<String>來說葱跋,List就是原始類型)持寄,所有的原始類型都是通過字節(jié)碼文件類Class類進行抽象。java.lang.Class類的一個具體對象就代表一個指定的原始類型娱俺。(eg: String原始類型抽象類即為:String.class....)泛型出現(xiàn)后的類型
泛型<>類型出現(xiàn)之后稍味,就擴充了數(shù)據(jù)類型。即在原始類型的基礎上荠卷,添加泛型的特性模庐,就能代表或者說表達更多的數(shù)據(jù)類型。從只有原始類型擴充了參數(shù)化類型(ParameterizedType)油宜,類型變量類型(TypeVariable)掂碱,泛型限定的參數(shù)化類型(含通配符+通配符限定表達式。即 ? extends , super等)慎冤,泛型數(shù)組類型(GenericArrayType)疼燥。3. 與泛型有關的類型不能和原始類型統(tǒng)一到Class的原因(泛型類型與對應的原始類型對應相同的Class)
上面這個原因要說的意思是:泛型類型所代表的Class對象與對應的原始類型對應的Class是同一個對象,例如:List<String>,List<Integer>..等等與原始類型List蚁堤,所代表的Class類都是java.util.List醉者。因為會有"類型擦除"原因。
【1】產生泛型擦除的原因
本來新產生的類型+原始類型都應該統(tǒng)一成各自的字節(jié)碼文件Class類型對象披诗,但是由于泛型不是最初的Java的基礎部分撬即,如果真的要加入的真正的泛型,會涉及到JVM指令集的修改藤巢,這樣代價就太大了搞莺。多說一句:在java中的泛型并不是代表著真正的新的類型,只是一個語法糖掂咒,便于類型檢查和編譯器優(yōu)化的手段而已才沧。
【2】Java中如何引入泛型
為了使用泛型的優(yōu)勢而又不真正引入泛型,java采用“泛型擦除”的機制來引入泛型這個語法糖绍刮。所以Java中的泛型僅僅是給編譯器javac使用的温圆,用來確保數(shù)據(jù)的安全性和免去強制類型轉換的麻煩。結果就是孩革,一旦含有泛型聲明和定義使用的代碼地方岁歉,經過編譯完成之后,所有和泛型有關的類型就會被全部擦除膝蜈,也就是被清理掉锅移。(List<String> list = new ArrayLIst<String>()
會編譯成 List list = new ArrayList( ))
熔掺。
【3】Class不能表達與泛型有關的類型
由于編譯器的"擦除"機制,就造成了與泛型有關的如參數(shù)化類型非剃,類型變量類型置逻,泛型限定的參數(shù)化類型,數(shù)組類型备绽,都會被全部打回原形券坞,在字節(jié)碼文件中中全部都是泛型擦除后的原始類型,并不存在和自身類型一直的字節(jié)碼文件肺素。所以恨锚,和泛型相關的新擴充進行的類型不能被統(tǒng)一到Class類中,即List<String>并不能用類似List<String>.class這個Class類文件來表示類型倍靡。
【4】 與泛型有關的類型在java中的表示
為了通過反射操作這些類型已迎合實際的開發(fā)需要猴伶,Java就新增加了ParameterizedType,GenericArrayType,TypeVariable,WildcardType
這幾種類型來代表不能被歸一到Class類中的類型菌瘫,但是又是和原始類型不同且齊名同等級的類型蜗顽。
【5】 Type接口的引入:統(tǒng)一與泛型有關的類型和原始類型Class
- 引入Type的原因
為了程序的擴展性,最終引入了Type接口作為Class,ParameterizedType雨让,GenericArrayType,TypeVariable,WildcardType這幾種類型的公共父接口雇盖。這樣實現(xiàn)了Type接口類型參數(shù),并可以接收以上五種子類的實參或者返回值類型就是Type型的參數(shù)栖忠。 - Type接口中沒有方法的原因
從上面看到崔挖,Type接口出現(xiàn)僅僅起到了通過多態(tài)來達到程序類型擴展性提高的作用,并沒有其他的作用庵寞。所有狸相,沒有必要提供任何方法。
泛型種類與使用
在Java中捐川,接口Type用于表示所有類型的高級公共接口脓鹃,也是Class類也實現(xiàn)了該接口。Type類型的所有子類型包括:原始類型古沥,參數(shù)化類型瘸右,數(shù)組類型,類型變量和基本類型岩齿。
Type中包括了以下幾種實現(xiàn):
public interface Type {
}
//說明class類可以轉化為Type類型
public final
class Class<T> implements java.lang.reflect.Type...
Type(所有子接口)
-GenericArrayType
-ParameterizedType
-TypeVariable<D extends GenericDeclaration>
-WildcardType
下面分別總結每種類型的含義和使用場景太颤。
參數(shù)化類型-原始類型
在Java中,參數(shù)化類型使用java.lang.reflect.ParameterizedType表示盹沈。其源代碼如下龄章,比較重要的或者說通常使用的是前兩個:
public interface ParameterizedType extends Type {
//獲取參數(shù)化類型中的實際參數(shù)類型(eg. String.class..)
Type[] getActualTypeArguments();
//獲取參數(shù)化類型中的原始類型
Type getRawType();
下面通過一段示例代碼來說明:
import Java.util.*;
//聲明有泛型的類
public class Pair<T> {
private final T first;
private final T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T first() {
return first;
}
public T second() {
return second;
}
//申明返回List<String>的集合
public List<String> stringList() {
return Arrays.asList(String.valueOf(first), String.valueOf(second));
}
public static void main(String[] args) {
Pair p = new Pair<Object>(23, "skidoo");
for (String s : p.stringList())
System.out.print(s + " ");
}
}
上述程序會報錯,錯誤在聲明p變量時候,沒有指明參數(shù)類型做裙。這段程序沒多大實用效果岗憋,僅僅用來說明。
若是在程序中有如下聲明:List<String> list = new ArrayList<String>()
的時候菇用,對于List<String>來說捐腿,List集合類型就是原始類型贸辈,集合內部元素類型被參數(shù)固定的List<String>就是參數(shù)化類型,即參數(shù)化類型ParameterizedType也是一種類型鸟废,但是是包含內部元素類型被固化的一種類型悍缠,通常都是基于泛型<>來聲明的卦绣。關于代碼中泛型類型使用,有以下幾點需要說明:
- Java參數(shù)化類型相對于C++的參數(shù)化類型而言飞蚓,有本質區(qū)別滤港。前者只是編譯器的類型檢查的一種手段,而后者則是真正的不利于原始類型的新的類型趴拧。
- 一個原生類型很像其對應的參數(shù)化類型溅漾,但是它的所有實例成員都要被替換掉,而替換物就是這些實例成員被擦除掉對應部分之后剩下的東西著榴。具體地說添履,在一個實例方法聲明中出現(xiàn)的每個參數(shù)化的類型都要被其對應的原生部分所取代。我們程序中的變量p是屬于原生類型Pair的脑又,所以它的所有實例方法都要執(zhí)行這種擦除暮胧。這也包括聲明返回List<String>的方法stringList。編譯器會將這個方法解釋為返回原生類型List问麸。
//源代碼中聲明
List<String> list = new ArrayList<Stirng>();
list.add("test");
//編譯器編譯后class字節(jié)反匯編得到如下:
List list = new ArrayList();
list.add(String.valueOf("test");
//也就說明了類型擦出和參數(shù)化類型實際上往衷,僅僅參數(shù)化類型與原始類型差不多,僅僅是參數(shù)化類型可以在我們寫代碼時候严卖,在編譯期間進行類型檢查而已席舍,僅僅是編譯器類型安全檢查的一種機制。
- 不同的類型參數(shù)組成的泛型類型哮笆,其class的類型都和原始類型的class的類型完全相同来颤。也說明了不同類型參數(shù)組成的參數(shù)化類型之間可以進行強制類型轉換。
接下來疟呐,再看看如何在程序中使用ParameterizedType脚曾。可以通過該類獲取參數(shù)化類型中類型參數(shù)的實際類型启具,也就是對應的Class對象本讥。
public class GenericParamType {
public static void applyMethod(List<Date> list) throws Exception
{
Method m = GenericParamType.class.getMethod("applyMethod",List.class);
// 其返回是參數(shù)的類型
Type[] t1 = m.getParameterTypes();
//其返回的是參數(shù)的參數(shù)化的類型,里面的帶有實際的參數(shù)類型
Type[] t2 = m.getGenericParameterTypes();
Method m2 = GenericParamType.class.getMethod("main",String[].class);
//參數(shù)里面如果不是參數(shù)化類型的話,那么 getGenericParameterTypes就返回與 getParameterTypes 一樣
Type[] t3 = m2.getGenericParameterTypes();
Type[] t4 = m2.getParameterTypes();
out.println(t1[0]);//interface java.util.List
out.println(t2[0]);//java.util.List<java.util.Date>
//我們通過 getGenericParameterTypes 得到的是 List<Date>,那么我們怎么能得到它的參數(shù)化類型的實例呢
// type 還有好多子接口拷沸,我們通過子接口來操作
out.println(t2.getClass());
ParameterizedType t = (ParameterizedType)t2[0];//將類型向參數(shù)化類型轉換
out.println(t.getClass());
out.println(t.getActualTypeArguments()[0]);// 可以得到參數(shù)化類型的參數(shù)實例
}
}
若是復合的參數(shù)化類型色查,還可以迭代里面的參數(shù)化類型出來:
public class ReflectDemo {
public static void main(String[] args) throws Exception{
Proxy target = new ReflectDemo.Proxy();
Method m = target.getClass().getDeclaredMethod("parameter", List.class,String[].class,Integer.class);
Type[] types = m.getGenericParameterTypes();
Assert.assertEquals(Modifier.PUBLIC, m.getModifiers());
Class<?>[] parameterTypes = m.getParameterTypes();
System.out.println("method's parameter class object>>"+parameterTypes.length);
for(Type t : types) {
if(t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] actypes = pt.getActualTypeArguments();
for(Type tt : actypes) {
if(tt instanceof ParameterizedType){
ParameterizedType ptt = (ParameterizedType) tt;
Type[] pttype = ptt.getActualTypeArguments();
for(Type pttt : pttype) {
System.out.println(pttt.toString());
}
}
System.out.println(tt.toString());
}
}
}
}
static class Proxy{
private Map<String,Object> map;
public void parameter(List<Map<String,Object>> list,String[] str,Integer it){
}
}
}
數(shù)組類型
在java中,代表數(shù)組類型的類是GenericArrayType.只有諸如Type[][]...形式的至少有一個[]即代表為數(shù)組的類型才能轉化成GenericArrayType撞芍。內部只有一個方法秧了,用于查詢數(shù)組中的元素類型:
//數(shù)組類接口定義
public interface GenericArrayType extends Type {
Type getGenericComponentType();
}
public class GenType<E> {
public String[] str;
public E[] e1;
public ArrayList<E>[] ale;
public E[][] e2;
//不可轉成GenericArrayType,class [[Ljava.lang.Integer;
public Integer[][] ins;
//不可轉成GenericArrayType,class [Ljava.lang.String;
public String[] str;
public static void main(String[] args) throws Exception{
Type type = GenType.class.getDeclaredField("ale").getGenericType();
System.out.println(type.toString());
//從上一個輸出可以看到是一個參數(shù)化類型或者類型變量數(shù)組類型序无,才可以強轉
//并且验毡,基本類型對象數(shù)組也不能轉為數(shù)組類型,因為代表固定class對象
System.out.println(((GenericArrayType)type).getGenericComponentType());
}
}
//輸出
java.util.ArrayList<E>[]
java.util.ArrayList<E>
關于這個數(shù)組類型GenericArrayType需要說明的是:
- 無論從左向右有幾個[]并列帝嗡,這個方法僅僅脫去最右邊的[]之后剩下的內容就作為這個方法的返回值.
- 在通過反射獲取得到類型是參數(shù)化類型數(shù)組或者是類型變量數(shù)組類型晶通,才可以強轉換為GenericArrayType數(shù)組類型對象,對內部的實際元素類型進行查詢和處理哟玷;而主類型是基本數(shù)據(jù)類型對象或者String類型的數(shù)組狮辽,是不能轉成數(shù)組類型的,因為他們已經是原子類型巢寡。
類型變量
在java中類型變量用類TypeVariable表示喉脖。類型變量接口中有個方法getBounds()是用來獲取上邊界類型,原因是在類型變量定義時候只能使用extends關鍵字進行多邊界限定抑月,而不能使用super树叽,所以關鍵字extends都代表的是類型變量的上邊界。
若是將類型變量具體化爪幻,例如定義一個類Person<E>
,那么E就代表這個Person類的類型變量菱皆,變量變量,即為可以變的類型挨稿,可以變就代表我們在創(chuàng)建Person實例的時候仇轻,可以傳入不同類型的變量來進行設置。eg: Person<String>, Person<Integer>..
等等奶甘。傳入的類型不同篷店,那么,容器元素的類型也就固化了臭家,在編譯期就能檢查錯誤疲陕。
關于類型邊界上界說明看例子:
public static <E extends Map<String, Date>& Cloneable&Serializable> E methodVI(E e){
return null;
}
- E的第一個上邊界就是Map<String,Date>,是參數(shù)化ParameterizedType類型
- E的第二個上邊界是Cloneable钉赁,是Class類型
- 因為類型變量可以通過&進行多個上邊界限定蹄殃,因此上邊界有多個,因此返回值類型是數(shù)組類型[ ]
看看使用例子:
public class GenType<E> {
public Map<String,Object>[] map;
public E[] e1;
public ArrayList<String>[] ale;
public E[][] e2;
public Integer[][] ins;//不可轉成GenericArrayType
public String[] str;//不可轉成GenericArrayType
public static <E extends Map<String, Date>& Cloneable&Serializable> E methodVI(E e){
return null;
}
public static void main(String[] args) throws Exception{
Type type = GenType.class.getDeclaredField("e1").getGenericType();
System.out.println(type.toString());
System.out.println(((GenericArrayType)type).getGenericComponentType());
System.out.println(((GenericArrayType)type).getGenericComponentType().getClass());
//類型變量TypeVariable.getBounds()獲取類型邊界
System.out.println(((TypeVariable)((GenericArrayType)type).getGenericComponentType()).getBounds()[0]);
}
}
//輸出
E[]
E
class sun.reflect.generics.reflectiveObjects.TypeVariableImpl
class java.lang.Object
通配符類型
表示通配符類型的類型你踩,表示為wildcardType诅岩,通常用符號?來表示匹配讳苦。通常該類用來限定泛型類型的邊界,包括上界和下界吩谦。
public interface WildcardType extends Type {
//上界鸳谜,? extends ...
Type[] getUpperBounds();
//下界,? super ....
Type[] getLowerBounds();
}
至于邊界的返回值為什么聲明為Type類型式廷,可以從下面例子中看到:
-
public static void printColl(ArrayList<? extends ArrayList<String>> al){}
咐扭,其中通配符表達式是?extends ArrayList<String>
,這樣extends后面是?的上邊界,這個上邊界是參數(shù)化類型ParameterizedType滑废。 -
public static <E> void printColl(ArrayList<? extends E> al){}
蝗肪,其中通配符表達式是? extends E>
,這樣extends后面是?的上邊界,這個上邊界是類型變量TypeVariable類型策严。 -
public static <E> void printColl(ArrayList<? extends E[]> al){}
穗慕,其中通配符表達式是? extends E[]>
,這樣extends后面是?的上邊界,這個上邊界是類型變量GenericArrayType類型妻导。 -
public static <E> void printColl(ArrayList<? extends Number> al){}
,其中通配符表達式是? extends Number>
,這樣extends后面是?的上邊界怀各,這個上邊界是類型變量Class類型倔韭。
所以,綜上的不同類型瓢对,得到的都是Type接口的子實現(xiàn)類寿酌。
extends與super
- <? extends T> : 是指上界通配符(Upper Bounds Wildcards)
- <? super T>: 是指下界通配符(Lower Bounds Wildcards)
- 子類轉換為父類(小轉大)是隱式的,而父類轉換成子類硕蛹,需要顯示手動強制轉換醇疼。
- PECS(Producer Extends Consumer Super)原則:
- 頻繁往外讀取內容的,適合用上界Extends法焰。
- 經常往里插入的秧荆,適合用下界Super。
- List<? extends E>表示該list集合中存放的都是E的子類型(包括E自身)埃仪,由于E的子類型可能有很多乙濒,但是我們存放元素時實際上只能存放其中的一種子類型(這是為了泛型安全,因為其會在編譯期間生成橋接方法<Bridge_Methods>該方法中會出現(xiàn)強制轉換卵蛉,若出現(xiàn)多種子類型颁股,則會強制轉換失敗)傻丝,例子如下:
List<? extends Number> list=new ArrayList<Number>();
list.add(4.0);//編譯錯誤
list.add(3);//編譯錯誤
上例中添加的元素類型不止一種甘有,這樣編譯器強制轉換會失敗,為了安全葡缰,Java只能將其設計成不能添加元素亏掀。雖然List<? extends E>不能添加元素忱反,但是由于其中的元素都有一個共性--有共同的父類,因此我們在獲取元素時可以將他們統(tǒng)一強制轉換為E類型幌氮,我們稱之為get原則缭受。
- 對于List<? super E>其list中存放的都是E的父類型元素(包括E),我們在向其添加元素時该互,只能向其添加E的子類型元素(包括E類型)米者,這樣在編譯期間將其強制轉換為E類型時是類型安全的,因此可以添加元素宇智,例子如下:
List<? super Number> list=new ArrayList<Number>();
list.add(2.0);
list.add(3.0);
但是蔓搞,由于該集合中的元素都是E的父類型(包括E),其中的元素類型眾多随橘,在獲取元素時我們無法判斷是哪一種類型喂分,故設計成不能獲取元素,我們稱之為put原則机蔗。實際上蒲祈,我們采用extends,super來擴展泛型的目的是為了彌補例如List<E>只能存放一種特定類型數(shù)據(jù)的不足萝嘁,將其擴展為List<? extends E> 使其可以接收E的子類型中的任何一種類型元素梆掸,這樣使它的使用范圍更廣。
public static void main(String[] args) {
//裝蘋果的盤子不是裝水果的盤子牙言,不等價
// Plate<Fruit> fp = new Plate<Apple>(new Apple());
//Plate<? extends Fruit> 是Plate<Fruit> 以及Plate<Apple>的基類
Plate<? extends Fruit> fp = new Plate<Apple>(new Apple());
// fp.setItem(new Apple()); //error
// fp.setItem(new Fruit()); //error
Fruit f = fp.getItem();
f.outf();
Object of = fp.getItem();
Apple apple = (Apple) fp.getItem();//父類強制轉化為子類
apple.outa();
System.out.println(apple);
// Plate<? super Fruit> sf = new Plate<Apple>(new Apple()); //error
//sf,getItem() >> generic.Fruit@xxx
Plate<? super Fruit> sf = new Plate<Fruit>(new Fruit());
//sf,getItem() >> generic.Apple@xxx
//強制轉換 sf1 = new Plate<Fruit>((Fruit)new Apple())
Plate<? super Fruit> sf1 = new Plate<Fruit>(new Apple());
((Fruit)sf.getItem()).outf();
System.out.println(sf1.getItem().getClass());
((Apple)sf1.getItem()).outa();
sf.setItem(new Apple());
}
//輸出
fruit
apple
generic.Apple@4eb09321
fruit
class generic.Apple
apple
設計這玩意酸钦,除了能讓給程序員加點語法糖,在處理這些集合類型時候咱枉,能夠靈活多變的存取不同類型的對象卑硫;另一方面,也為了程序安全蚕断,在編譯器編譯過程中就要對這些容器中元素類型進行類型檢查欢伏,盡早發(fā)現(xiàn)錯誤并反饋給程序員。
原生類型 和 參數(shù)化類型
反射得到參數(shù)化類型中的類型參數(shù)
Java 泛型 <? super T> 中 super 怎么 理解基括?與 extends 有何不同颜懊?