通配符
1.數(shù)組具有協(xié)變性:可以向?qū)С鲱愋偷臄?shù)組賦予基類型的數(shù)組引用:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
class CovariantArrays {
public static void main(String... args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); //OK
fruit[1] = new Jonathan(); //OK
fruit[2] = new Orange(); //ArrayStoreException
}
}
上面的代碼不會(huì)出現(xiàn)編譯問(wèn)題席噩,因?yàn)?strong>Apple、Orange、Jonathan都是Fruit的子類型,Fruit類型的引用持有它們并沒(méi)有任何問(wèn)題忧设,是有意義的。但是fruit[2] = new Orange();這一句在運(yùn)行時(shí)會(huì)拋出ArrayStoreException異常颠通,因?yàn)閿?shù)組fruit在運(yùn)行時(shí)的實(shí)際類型為Apple址晕。
2.數(shù)組的協(xié)變性對(duì)List并不起作用:
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());
上面代碼中唯一的限制就是這個(gè)List要持有某種具體的Fruit或Fruit的子類型,但是編譯器實(shí)際上并不知道LIst持有什么類型顿锰,那么也就不能安全地向其中添加對(duì)象谨垃,因此會(huì)出現(xiàn)編譯時(shí)錯(cuò)誤。
一.編譯器有多聰明
1.雖然在上面的程序中List的add()方法不可用硼控,但是并不是所有的方法都是不可用的:
public class CompilerIntelligence {
public static main(String... args) {
List<? extends Fruit> flist = Arrays.asList(new Apple());
Apple a = (Apple) flist.get(0);
flist.contains(new Apple());
flist.indexOf(new Apple());
}
}
這兩段程序的區(qū)別在于add()方法將接受一個(gè)具有泛型參數(shù)類型的參數(shù)刘陶,但是contains()和indexOf()將返回或者接受Object類型的參數(shù)。因此淀歇,在指定一個(gè)ArrayList<? extends Fruit>時(shí)易核,add()的參數(shù)就變成了"? extends Fruit"匈织,此時(shí)浪默,編譯器并不能了解這里需要Fruit的哪個(gè)具體子類型,因此它不會(huì)接受任何類型的Fruit缀匕。而另外的方法使用了Object纳决,并不涉及通配符,因此編譯器也將允許這個(gè)調(diào)用乡小。而上面的get()方法只會(huì)也只能返回Fruit對(duì)象阔加,這是在該泛型參數(shù)所給定了邊界——“任何擴(kuò)展自Fruit的對(duì)象”之后所能做的唯一的事情了。
二.逆變
1.超類通配符:使用方法是由某個(gè)特定類的任何基類來(lái)界定的即<? super MyClass>满钟。
2.有了超類通配符之后胜榔,程序可以這樣寫(xiě):
List<? super Apple> apples = Arrays.asList(new Apple());
apples.add(new Apple());
apples.add(new Jonathan());
apples.add(new Fruit()); //Error
這里Apple是下接,那么向其中添加Apple或Apple的子類型是安全的湃番,但是添加Fruit是不安全的夭织。
三.無(wú)界通配符
1.第一種情況下無(wú)界通配符意味著“任何事物”,即編譯器很少關(guān)心使用的是原生類型還是<?>吠撮。因此尊惰,<?>是在聲明:我是想用Java的泛型來(lái)編寫(xiě)代碼,我在這里并不是要用原生類型,但是在當(dāng)前這種情況下弄屡,泛型參數(shù)可以持有任何類型题禀。
2.泛型的另一種應(yīng)用是:當(dāng)你在處理多個(gè)參數(shù)時(shí),優(yōu)勢(shì)允許一個(gè)參數(shù)可以時(shí)任喝類型膀捷,同時(shí)為其他參數(shù)確定某種特定類型迈嘹,如Map<String, ?> map = new HashMap<String, Integer>
。
3.List實(shí)際表示“持有任何Object類型的List”全庸,而List<?>表示“具有某種特定類型的非原生List江锨,只是我們不知道那種類型是什么「馄”
4.使用確切類型來(lái)代替通配符啄育,可以使用泛型參數(shù)來(lái)做更多的事,但是使用通配符使得你必須接受范圍更寬的參數(shù)化類型作為參數(shù)拌消。
一.捕獲轉(zhuǎn)換
1.捕捉轉(zhuǎn)換:如果向一個(gè)使用<?>的方法傳遞原生類型挑豌,那么對(duì)編譯器來(lái)說(shuō),可能會(huì)推斷出實(shí)際的類型參數(shù)墩崩,使得這個(gè)方法可以回轉(zhuǎn)并使用另一個(gè)使用這個(gè)確切類型的方法氓英。
public class CaptureConversion {
static <T> void f1(Holder<T> holder) {
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder<?> holder) {
f1(holder);
}
@SupressWarnings("unchecked")
public static void main(String... args) {
Holder raw = new Holder<Integer>(1);
//f1(raw); //warnings
f2(raw); //no warnings
}
上面代碼的執(zhí)行過(guò)程是:參數(shù)類型在調(diào)用f2()的過(guò)程中被捕獲,因此它可以在對(duì)f1()對(duì)調(diào)用中被使用鹦筹。
問(wèn)題
一.任何基本類型都不能作為類型參數(shù)
1.不能將基本類型用作類型參數(shù)铝阐,因此不能創(chuàng)建ArrayList<int>之類的東西。
2.上一條的解決方法是使用基本類型的包裝器以及自動(dòng)包裝機(jī)制铐拐。比如徘键,創(chuàng)建一個(gè)ArrayList<Integer>,并將基本類型int應(yīng)用于這個(gè)容器遍蟋。
3.但是吹害,自動(dòng)包裝機(jī)制不能應(yīng)用于數(shù)組,因此當(dāng)需要使用數(shù)組的時(shí)候需要注意虚青。
二.實(shí)現(xiàn)參數(shù)化接口
1.一個(gè)類不能實(shí)現(xiàn)同一個(gè)泛型接口的兩種變體它呀,由于擦除的原因,這兩個(gè)變體會(huì)成為相同的接口:
interface Payable<T> {}
class Employee implements Payable<Employee> {}
class Hourly extends Employee implements Payable<Hourly> {}
由于擦除會(huì)將Payable<Employee>和Payable<Hourly>簡(jiǎn)化為相同的類Payable棒厘,也就意味著上面的代碼重復(fù)兩次地實(shí)現(xiàn)了相同的接口纵穿,因此不能編譯。但是奢人,如果將泛型參數(shù)全都移除谓媒,上述代碼是可以編譯的。
三.轉(zhuǎn)型和警告
1.使用帶有泛型參數(shù)類型的轉(zhuǎn)型或instanceof不會(huì)帶有任何效果:
class Stack<T> {
private int index = 0;
private Object[] storage;
......
@SupressWarnings("unchecked")
public T pop() {
return (T) storage[--index];
}
}
在pop()中达传,由于擦除的原因篙耗,編譯器無(wú)法知道這個(gè)轉(zhuǎn)型是否是安全的迫筑,并且由于T被擦除到它的第一個(gè)邊界,此處即Object宗弯,因此pop()實(shí)際上只是將Object轉(zhuǎn)型為Object脯燃,相當(dāng)于沒(méi)有執(zhí)行任何轉(zhuǎn)型。
四.重載
public class UseList<W, T> {
void f(List<W> w) {}
void f(List<T> t) {}
}
上面的代碼由于擦除的原因蒙保,重載的方法將產(chǎn)生相同的簽名辕棚,因此是不能編譯的。
五.基類劫持了接口
public class ComparablePet implements Comparable<ComparablePet> {
public int compareTo(ComparablePet arg) {
return 0;
}
}
class Cat extends ComparablePet implements Comparable<Cat> {
public int compareTo(Cat arg) {
return 0;
}
}
這里有一個(gè)可以進(jìn)行比較的Pet類邓厕,這里我們的想法是對(duì)它的子類Cat的比較盡興窄化逝嚎。但是會(huì)出現(xiàn)問(wèn)題,因?yàn)橐坏?strong>Comparable確定了ComparablePet參數(shù)详恼,那么其他任何實(shí)現(xiàn)類都不能與ComparablePet之外的任何對(duì)象進(jìn)行比較补君。
自限定的類型
一.古怪的循環(huán)泛型
1.不能直接集成一個(gè)泛型參數(shù),但是昧互,可以繼承在自己的定義中使用這個(gè)泛型參數(shù)的類挽铁,如:
class GenericType<T> {}
public class CuriouslyRecurringGener extends GenericType<CuriousRecurringGener> {}
這就是古怪的循環(huán)泛型(CRG):類相當(dāng)古怪地出現(xiàn)在它自己基類中這一事實(shí)。
2.CRG的本質(zhì):基類用導(dǎo)出類替代其參數(shù)敞掘。這意味著泛型基類變成了一種其所有導(dǎo)出類的公共功能的模版叽掘,但是這些功能對(duì)于其所有參數(shù)和返回值,將使用導(dǎo)出類型:
class BasicHolder<T> {
T element;
void set(T arg) {
this.element = arg;
}
T get() {
return element;
}
}
class SubType extends BasicHolder<SubType> {
public static void main(String... args) {
SubType st1 = new SubType(), st2 = new SubType();
st1.set(st2);
SubType st2 = st1.get();
}
}
二.自限定
1.自限定:
class SelfBounded<T extends SelfBounded<T>> {}
SelfBounded類接受泛型參數(shù)T玖雁,而T由一個(gè)邊界限定更扁,這個(gè)邊界就是擁有T作為其參數(shù)的SelfBounded。
2.自限定強(qiáng)制泛型當(dāng)作自己的邊界參數(shù)來(lái)使用赫冬。如:
class A extends SelfBounded<A> {}
3.自限定限制只能強(qiáng)制作用于繼承關(guān)系浓镜。如果食用自限定,就應(yīng)該了解這個(gè)類所用的類型參數(shù)將與使用這個(gè)參數(shù)的類具有相同的基類型面殖。這會(huì)強(qiáng)制要求使用這個(gè)類的每個(gè)人都要遵循這種形式竖哩。
三.參數(shù)協(xié)變
1.自限定類型的價(jià)值在于它們可以產(chǎn)生協(xié)變參數(shù)類型——方法參數(shù)類型后隨子類而變化:
interface Generic<T extends Generic<T>> {
T get();
void set(T t);
}
interface GetterAndSetter extends Generic<GetterAndSetter> {}
public class Generics {
void test(Getter g) {
GetterAndSetter result = g.get();
Generic g = g.get();
GetterAndSetter gns = new GetterAndSetter();
gns.set(result);
gns.set(g); // Can't compile
}
}
上面代碼中result和gg的實(shí)際類型都是GetterAndSetter,同時(shí)脊僚,set()方法只能接受GetterAndSetter類型的參數(shù),如果傳入基類參數(shù)遵绰,編譯不會(huì)通過(guò)辽幌。但是注意,這段代碼由于使用了自限定類型產(chǎn)生子類類型相同的換回類型椿访,只有在囊括了協(xié)變返回類型的Java SE5的環(huán)境下才能編譯乌企。