1.泛型的由來
一般的類和方法踢匣,只能使用具體的類型似枕,要么是基本數(shù)據(jù)類型,要么是自定義的類型趣苏,如果要編寫可以適用于多種類型的代碼狡相,這種刻板的限制對代碼的束縛性就很大。
好的程序可以適應更多的場景食磕,來滿足我們對業(yè)務的需求尽棕。為了滿足這種要求。Java給我們提供了很多方式來滿足這種靈活性彬伦,擴展性滔悉,通用性的要求。
最典型的例子就是多態(tài)单绑,在任何面向?qū)ο蟮木幊陶Z言中都有多態(tài)的機制回官。例如,我們可以在方法的形參中聲明某個基類搂橙,而在實際調(diào)用的時候可以給他傳遞該基類的導出類作為實際參數(shù)歉提。這樣的方法就會更加通用一些。其實受限于java的單繼承體制,我們更好的做法是聲明一個接口(interface)作為方法的參數(shù)苔巨。將實現(xiàn)該接口的導出類作為實參傳遞進行版扩,進而獲得更大的靈活性。
但是不幸的是侄泽,有時候礁芦,即使我們依附于多態(tài)的特性,使用接口來實現(xiàn)更加靈活的程序蔬顾。還是沒法滿足我們的需求宴偿。因為一旦指明了接口湘捎,那么程序就要求你必須使用某種特定的接口诀豁。而我們希望達到的目的是編寫更加通用的代碼。要使代碼應用于"某種不具體的類型",而不是具體的接口和類窥妇。
這時候我們今天的主角“泛型”就出場了舷胜,泛型是Java SE5帶來的新特性。泛型實現(xiàn)了參數(shù)化類型的概念活翩,是代碼可以應用于多種類型烹骨。而“泛型”這個術語的意思其實就是"可以適用于很多種類型"。
2.泛型的分類
2.1泛型類
其實有很多的原因促成了泛型的出現(xiàn)材泄,其中最終要的原因是沮焕,為了創(chuàng)建容器類,容器就是要存放使用對象的地方拉宗。我們常見的容器類有List,Set,Map等等這些集合類都是容器類峦树。當然數(shù)組也是,只不過數(shù)組具有固定的大小旦事。靈活性不夠高魁巩。
假設現(xiàn)在我們沒有泛型,我們想定義一個容器姐浮,在容器中持有某種水果谷遂。(你可以把容器想象成盤子,或者碗卖鲤,任何可以容納水果的器皿)肾扰,我們先讓容器持有一個橘子(在一個碗里面放一個橘子)。
public class Hoder {
private Orange orange;
// 構(gòu)造器蛋逾,get/set方法省略
}
但是這時候我不想放橘子了集晚,我想放置一個蘋果,這時候程序是這樣的
public class Hoder {
private Apple apple;
// 構(gòu)造器换怖,get/set方法省略
}
可以看到這樣的程序很不靈活甩恼,擴展性,通用度也不高,我們沒法通過容器持有自己想要的對象条摸。這時候通過泛型就可以解決這樣的問題悦污。
public class Hoder<T> {
private T item;
public Hoder(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
- T 表示 類型參數(shù),用尖括號括起來放在類型的后面钉蒲。
- 這時候我們就可以暫時不指定類型切端,等稍后使用的時候再去決定使用什么樣的類型。
- 這里的T是Type的縮寫顷啼,表示類型踏枣。
2.2泛型接口
泛型不僅可以應用于類也可以應用于接口。例如生成器(Generator),實際上這是工廠方法設計模式的一種應用钙蒙,不過當使用設國車給其創(chuàng)建新對象的時候不需要任何的參數(shù)茵瀑。而工廠方法一般需要參數(shù)。也就是說無需需要額外的信息就可以創(chuàng)建新的對象躬厌。
定義一個Generator泛型接口马昨。
public interface Generator<T> {
T next();
}
再定義一些其他的類。
public class Coffee {
private static long counter = 0;
private final long id = counter++;
@Override
public String toString() {
return getClass().getSimpleName() + id;
}
}
public class Americano extends Coffee {
}
public class Breve extends Coffee {
}
public class Cappuccino extends Coffee {
}
public class Latte extends Coffee {
}
public class Mocha extends Coffee {
}
這時候我們可以編寫一個類扛施,實現(xiàn)Generator<Coffee>接口鸿捧,他能夠隨機生成不同的Coffee對象
public class CoffeeGenerator implements Generator<Coffee>,Iterable<Coffee> {
private Class[] types = {Latte.class, Mocha.class, Cappuccino.class, Americano.class, Breve.class};
private static Random random = new Random();
private int size;
public CoffeeGenerator() {
}
public CoffeeGenerator(int size) {
this.size = size;
}
@Override
public Coffee next() {
try {
return (Coffee)types[random.nextInt(types.length)].newInstance();
} catch (Exception e) {
throw new RuntimeException();
}
}
@Override
public Iterator<Coffee> iterator() {
return new CoffeeIterator();
}
class CoffeeIterator implements Iterator<Coffee> {
int count = size;
@Override
public boolean hasNext() {
return count > 0;
}
@Override
public Coffee next() {
count--;
return CoffeeGenerator.this.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
public static void main(String[] args) {
CoffeeGenerator gen = new CoffeeGenerator();
for(int i = 0; i < 5; i++) {
System.out.println(gen.next());
}
for (Coffee c : new CoffeeGenerator(5)) {
System.out.println(c);
}
// 結(jié)果
/**
* Cappuccino0
* Breve1
* Breve2
* Breve3
* Breve4
* Latte5
* Americano6
* Mocha7
* Breve8
* Americano9
*/
}
}
2.3泛型方法
到目前為止,泛型都是應用于類上的疙渣。但是同樣的在類中包含匙奴,參數(shù)化的方法。
public <T> void fn(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethod gm = new GenericMethod();
gm.fn("Hello,World!");
gm.fn(1);
gm.fn(1.0);
gm.fn(1.23F);
gm.fn(gm);
// 結(jié)果
/**
* java.lang.String
* java.lang.Integer
* java.lang.Double
* java.lang.Float
* com.thikinjava.ch15generic.genericmethod.GenericMethod
*/
}
- 泛型方法和泛型類沒有任何關系妄荔。泛型方法所在的類既可以是泛型類泼菌,也可以不是泛型類。 也就是說懦冰,是否擁有泛型方法灶轰,與其所在的類是否有泛型沒有關系
- 無論何時,只要你能做到刷钢,盡量使用泛型方法笋颤。如果可以用泛型方法取代整個類進行泛化,那么就使用泛型方法内地。
- 對于static方法而言伴澄,無法訪問泛型類的參數(shù)類型,如果要讓其擁有訪問泛型的能力阱缓,就聲明其為泛型方法非凌。
- 在使用泛型類的時候巢钓,在實例化的時候通常要指定類型參數(shù)的值序仙。而使用泛型方法的時候不需要指名參數(shù)類型,編譯器會自動找出具體的類型榛瓮,這稱作"類型參數(shù)判斷",我們可以像調(diào)用普通方法一樣,調(diào)用泛型方法喉悴,就好像fn()被重載過無數(shù)次棱貌。在方法返回值前用尖括號括起來<T>來聲明一個泛型方法。
3.擦除機制
3.1 擦除的神秘面紗
觀察下面這個例子箕肃。
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
// 結(jié)果 true
}
ArrayList<String>和ArrayList<Integer>很容以被認為是不同的類型婚脱,不同的類型在
行為上肯定也有所不同。
java的泛型是使用擦除來實現(xiàn)的勺像。這意味著當你在使用泛型的時候障贸,任何具體的
類型信息都被擦除了,你唯一知道的就是你在使用一個對象吟宦,因為List<String>
和List<Integer>在運行時是相同的類型篮洁,這兩種形式被擦除成了"原生"的類型,即List
在泛型的內(nèi)部督函,無法獲得任何有關泛型參數(shù)類型的信息嘀粱。
再看下面這個例子激挪。
假設我們定義一個容器辰狡,持有一個對象,并且想調(diào)用這個對象的特定方法(FMethod類的f()方法)
public class FMethod {
public void f() {
System.out.println("f方法被調(diào)用");
}
}
public class Manipulator<T> {
private T obj;
public Manipulator(T obj) {
this.obj = obj;
}
public void invoke() {
obj.f(); // 編譯出錯 cannot resolve method f();
}
public static void main(String[] args) {
FMethod fMethod = new FMethod();
Manipulator<FMethod> manipulator = new Manipulator<>(fMethod);
manipulator.invoke();
}
}
這時候是不行的由于擦除機制垄分,泛型T會被擦除為Object,我們無法直接調(diào)用FMethod對象的f()方法宛篇。
有沒有什么辦法可以讓編譯器知道f()是FMethod類中的方法呢?薄湿?
是有的叫倍。我們在 Manipulator<T>的泛型后面加上 extend FMethod, 完整的類的聲明就像這樣 public class Manipulator<T extends FMethod> {...}
這段代碼的含義是 T 必須具有類型FMethod 或者是 FMethod的導出類,泛型類型參數(shù)將被擦除到他的第一個邊界豺瘤,即FMethod,就好像T被FMethod替換了一樣吆倦。
3.2擦除的前世今生
為了減少擦除的混淆,我們必須要知道的是坐求,擦除不是一個語言特定蚕泽,而是Java實現(xiàn)泛型的一種折中的解決方法。因為泛型不是在java語言出現(xiàn)時就有的組成部分桥嗤。這種折中是必須的也是痛苦的须妻。
在基于擦除的實現(xiàn)中,泛型類型只有在靜態(tài)類型檢查期間才出現(xiàn)泛领,在此之后荒吏,程序中的所有泛型類型都會被擦除,替換為他們的非泛型上界渊鞋,例如List<T>這樣的類型將會被替換為List,而普通的類型變量
在未指定邊界的情況下會被擦除為Object 绰更,擦除的核心動機是它將泛化的客戶端代碼可以用非泛化的類庫來替換瞧挤,我們不僅要考慮泛化了的程序,也要考慮儡湾,Java SE5之前非泛型類庫的代碼兼容性問題皿伺。為了這種兼容性,Java設計者們認為擦除是唯一可行的解決辦法盒粮,使得泛型代碼和非泛型代碼共存成為了可能鸵鸥。
3.3擦除的代價
擦除實現(xiàn)了從非泛化代碼向泛化代碼的過度,以及在不破壞現(xiàn)有類庫的情況下丹皱,將泛型融入Java語言妒穴。擦除使得現(xiàn)有的非泛型客戶端代碼能夠在不改變的情況下繼續(xù)使用。
在他給我?guī)砗芎玫木幊腆w驗的同時摊崭,隨之而來也面臨著一些問題讼油。泛型不能顯示的引用運行時類型的操作之中。如轉(zhuǎn)型呢簸,instanceof操作符矮台,new 表達式。因為有關參數(shù)的類型信息全部丟失了根时。
4.通配符
在了解通配符之前瘦赫,先來了解一下數(shù)組。Java 中的數(shù)組是協(xié)變的蛤迎,什么意思确虱?看下面的例子:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
fruit[0] = new Fruit(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
} catch(Exception e) { System.out.println(e); }
}
}
/* Output:
java.lang.ArrayStoreException: Fruit
java.lang.ArrayStoreException: Orange
*///:~
main 方法中的第一行,創(chuàng)建了一個 Apple 數(shù)組并把它賦給 Fruit 數(shù)組的引用替裆。這是有意義的校辩,Apple 是 Fruit 的子類,一個 Apple 對象也是一種 Fruit 對象辆童,所以一個 Apple 數(shù)組也是一種 Fruit 的數(shù)組宜咒。這稱作數(shù)組的協(xié)變,Java 把數(shù)組設計為協(xié)變的把鉴,對此是有爭議的故黑,有人認為這是一種缺陷。
盡管 Apple[] 可以 “向上轉(zhuǎn)型” 為 Fruit[]纸镊,但數(shù)組元素的實際類型還是 Apple倍阐,我們只能向數(shù)組中放入 Apple或者 Apple 的子類。在上面的代碼中逗威,向數(shù)組中放入了 Fruit 對象和 Orange 對象峰搪。對于編譯器來說,這是可以通過編譯的凯旭,但是在運行時期概耻,JVM 能夠知道數(shù)組的實際類型是 Apple[]使套,所以當其它對象加入數(shù)組的時候就會拋出異常。
泛型設計的目的之一是要使這種運行時期的錯誤在編譯期就能發(fā)現(xiàn)鞠柄,看看用泛型容器類來代替數(shù)組會發(fā)生什么:
// Compile Error: incompatible types:
ArrayList<Fruit> flist = new ArrayList<Apple>();
上面的代碼根本就無法編譯侦高。當涉及到泛型時, 盡管 Apple 是 Fruit 的子類型厌杜,但是 ArrayList<Apple> 不是 ArrayList<Fruit> 的子類型奉呛,泛型不支持協(xié)變。
使用通配符從上面我們知道夯尽,List<Number> list = ArrayList<Integer> 這樣的語句是無法通過編譯的瞧壮,盡管 Integer 是 Number的子類型。那么如果我們確實需要建立這種 “向上轉(zhuǎn)型” 的關系怎么辦呢匙握?這就需要通配符來發(fā)揮作用了咆槽。
4.1上邊界限定通配符
利用 <? extends Fruit> 形式的通配符,可以實現(xiàn)泛型的向上轉(zhuǎn)型:
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<Apple>();
// Compile Error: can’t add any type of object:
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); // Legal but uninteresting
// We know that it returns at least Fruit:
Fruit f = flist.get(0);
}
}
上面的例子中圈纺, flist 的類型是 List<? extends Fruit>秦忿,我們可以把它讀作:一個類型的 List, 這個類型可以是繼承了 Fruit 的某種類型蛾娶。注意灯谣,這并不是說這個 List 可以持有 Fruit 的任意類型。通配符代表了一種特定的類型茫叭,它表示 “某種特定的類型酬屉,但是 flist 沒有指定”。這樣不太好理解揍愁,具體針對這個例子解釋就是,flist 引用可以指向某個類型的 List杀饵,只要這個類型繼承自 Fruit莽囤,可以是 Fruit 或者 Apple,比如例子中的 new ArrayList<Apple>切距,但是為了向上轉(zhuǎn)型給 flist朽缎,flist 并不關心這個具體類型是什么。
如上所述谜悟,通配符 List<? extends Fruit> 表示某種特定類型 ( Fruit 或者其子類 ) 的 List话肖,但是并不關心這個實際的類型到底是什么,反正是 Fruit 的子類型葡幸,F(xiàn)ruit 是它的上邊界最筒。那么對這樣的一個 List 我們能做什么呢?其實如果我們不知道這個 List 到底持有什么類型蔚叨,怎么可能安全的添加一個對象呢床蜘?在上面的代碼中辙培,向 flist 中添加任何對象,無論是 Apple 還是 Orange 甚至是 Fruit 對象邢锯,編譯器都不允許扬蕊,唯一可以添加的是 null。所以如果做了泛型的向上轉(zhuǎn)型 (List<? extends Fruit> flist = new ArrayList<Apple>())丹擎,那么我們也就失去了向這個 List 添加任何對象的能力尾抑,即使是 Object 也不行。
另一方面蒂培,如果調(diào)用某個返回 Fruit 的方法蛮穿,這是安全的。因為我們知道毁渗,在這個 List 中践磅,不管它實際的類型到底是什么,但肯定能轉(zhuǎn)型為 Fruit灸异,所以編譯器允許返回 Fruit府适。
了解了通配符的作用和限制后,好像任何接受參數(shù)的方法我們都不能調(diào)用了肺樟。其實倒也不是檐春,看下面的例子:
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist =
Arrays.asList(new Apple());
Apple a = (Apple)flist.get(0); // No warning
flist.contains(new Apple()); // Argument is ‘Object’
flist.indexOf(new Apple()); // Argument is ‘Object’
//flist.add(new Apple()); 無法編譯
}
}
在上面的例子中,flist 的類型是 List<? extends Fruit>么伯,泛型參數(shù)使用了受限制的通配符疟暖,所以我們失去了向其中加入任何類型對象的例子,最后一行代碼無法編譯田柔。
但是 flist 卻可以調(diào)用 contains 和 indexOf 方法俐巴,它們都接受了一個 Apple 對象做參數(shù)。如果查看 ArrayList 的源代碼硬爆,可以發(fā)現(xiàn) add() 接受一個泛型類型作為參數(shù)欣舵,但是 contains 和 indexOf 接受一個 Object 類型的參數(shù),下面是它們的方法簽名:
public boolean add(E e)
public boolean contains(Object o)
public int indexOf(Object o)
所以如果我們指定泛型參數(shù)為 <? extends Fruit> 時缀磕,add() 方法的參數(shù)變?yōu)?? extends Fruit缘圈,編譯器無法判斷這個參數(shù)接受的到底是 Fruit 的哪種類型,所以它不會接受任何類型袜蚕。
然而糟把,contains 和 indexOf 的類型是 Object,并沒有涉及到通配符牲剃,所以編譯器允許調(diào)用這兩個方法遣疯。這意味著一切取決于泛型類的編寫者來決定那些調(diào)用是 “安全” 的,并且用 Object 作為這些安全方法的參數(shù)颠黎。如果某些方法不允許類型參數(shù)是通配符時的調(diào)用另锋,這些方法的參數(shù)應該用類型參數(shù)滞项,比如 add(E e)。
當我們自己編寫泛型類時夭坪,上面介紹的就有用了文判。下面編寫一個 Holder 類:
public class Holder<T> {
private T value;
public Holder() {}
public Holder(T val) { value = val; }
public void set(T val) { value = val; }
public T get() { return value; }
public boolean equals(Object obj) {
return value.equals(obj);
}
public static void main(String[] args) {
Holder<Apple> Apple = new Holder<Apple>(new Apple());
Apple d = Apple.get();
Apple.set(d);
// Holder<Fruit> Fruit = Apple; // Cannot upcast
Holder<? extends Fruit> fruit = Apple; // OK
Fruit p = fruit.get();
d = (Apple)fruit.get(); // Returns ‘Object’
try {
Orange c = (Orange)fruit.get(); // No warning
} catch(Exception e) { System.out.println(e); }
// fruit.set(new Apple()); // Cannot call set()
// fruit.set(new Fruit()); // Cannot call set()
System.out.println(fruit.equals(d)); // OK
}
} /* Output: (Sample)
java.lang.ClassCastException: Apple cannot be cast to Orange
true
*///:~
在 Holer 類中,set() 方法接受類型參數(shù) T 的對象作為參數(shù)室梅,get() 返回一個 T 類型戏仓,而 equals() 接受一個 Object作為參數(shù)。fruit 的類型是 Holder<? extends Fruit>亡鼠,所以set()方法不會接受任何對象的添加赏殃,但是 equals() 可以正常工作。
4.2下邊界限定通配符
通配符的另一個方向是 “超類型的通配符“: ? super T间涵,T 是類型參數(shù)的下界仁热。使用這種形式的通配符,我們就可以 ”傳遞對象” 了勾哩。還是用例子解釋:
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
}
writeTo 方法的參數(shù) apples 的類型是 List<? super Apple>抗蠢,它表示某種類型的 List,這個類型是 Apple 的基類型思劳。也就是說迅矛,我們不知道實際類型是什么,但是這個類型肯定是 Apple 的父類型潜叛。因此秽褒,我們可以知道向這個 List 添加一個 Apple 或者其子類型的對象是安全的,這些對象都可以向上轉(zhuǎn)型為 Apple威兜。但是我們不知道加入 Fruit 對象是否安全销斟,因為那樣會使得這個 List 添加跟 Apple 無關的類型。
在了解了子類型邊界和超類型邊界之后牡属,我們就可以知道如何向泛型類型中 “寫入” ( 傳遞對象給方法參數(shù)) 以及如何從泛型類型中 “讀取” ( 從方法中返回對象 )票堵。下面是一個例子:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src){
for (int i=0; i<src.size(); i++)
dest.set(i,src.get(i));
}
}
src 是原始數(shù)據(jù)的 List,因為要從這里面讀取數(shù)據(jù)逮栅,所以用了上邊界限定通配符:<? extends T>,取出的元素轉(zhuǎn)型為 T窗宇。dest 是要寫入的目標 List措伐,所以用了下邊界限定通配符:<? super T>,可以寫入的元素類型是 T 及其子類型军俊。
4.3無邊界通配符
還有一種通配符是無邊界通配符侥加,它的使用形式是一個單獨的問號:List<?>,也就是沒有任何限定粪躬。不做任何限制担败,跟不用類型參數(shù)的 List 有什么區(qū)別呢昔穴?
List<?> list 表示 list 是持有某種特定類型的 List,但是不知道具體是哪種類型提前。那么我們可以向其中添加對象嗎吗货?當然不可以,因為并不知道實際是哪種類型狈网,所以不能添加任何類型宙搬,這是不安全的。而單獨的 List list 拓哺,也就是沒有傳入泛型參數(shù)勇垛,表示這個 list 持有的元素的類型是 Object,因此可以添加任何類型的對象士鸥,只不過編譯器會有警告信息闲孤。