歡迎轉載,但請保留作者鏈接:http://www.itdecent.cn/p/f258c907019d
相關文章:
吐槽:這是目前最不深入的一篇了,因為關于泛型實在有太多需要注意的地方,本文僅人工過濾出了較常見的重點內(nèi)容,后期再進行不定時更新吧。
必要性
在程序日益復雜龐大的今天,編寫泛用性代碼的價值愈發(fā)變得巨大。
而要做到這一點,其訣竅僅只兩字而已——解耦。
最簡單的解耦,無疑是使用基類替代子類。然而由于Java僅支持單繼承,這種解耦方法所帶來的局限性未免過大,有種“只準投胎一次”的感覺。
使用接口替代具體類算是更進了一步,算是多給了一條命吧,但限制仍舊存在。要是我們所寫的代碼本身就是為了應用于“某種不確定的類型”呢?
這時候就輪到泛型登場了。
簡單泛型
雖然理想遠大。但Java引入泛型的初衷,也許只是為了創(chuàng)造容器類也說不定。
站在類庫設計者的角度,我們不妨走上一遭。
得益于單根繼承結構,我們可以這樣來設計一個持有單個對象的容器:
public class Holder1 {
private Object a;
public Holder1(Object a) {
this.a = a;
}
Object get() {
return a;
}
}
這個容器確實能持有多種類型的對象,但通常而言我們只會用它來存儲一種對象。也就是說雖然設計時希望能存儲任意類型,但使用時卻能夠只存儲我們想要的確定類型。
泛型可以達到這一目的,與此同時,這也能使編譯器為我們提供編譯期檢查。
class Automobile {}
public class Holder2<T> {
private T a;
public Holder2(T a) {
this.a = a;
}
public void set(T a) {
this.a = a;
}
public T get() {
return a;
}
public static void main(String[] args) {
Holder2<Automobile> h2 =
new Holder2<Automobile>(new Automobile());
Automobile a = h2.get(); // No cast needed
// h2.set("Not an Automobile"); // Error
// h2.set(1); // Error
}
}
如你所見,使用方法即為在類名后添加尖括號,然后填寫類型參數(shù)“T”。使用時用明確的類型參數(shù)替換掉“T”,即為該容器指定了其存儲的確定類型。
泛型方法
泛型可以應用于方法,只需要將泛型參數(shù)列表放在方法返回值之前即可。
下面這個例子中,f()的效果看起來像是重載過一樣:
//: generics/GenericMethods.java
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.0);
gm.f(1.0F);
gm.f(‘c’);
gm.f(gm);
}
} /* Output:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
GenericMethods
*///:~
能這樣做的原因在于編譯器擁有稱為類型參數(shù)推斷的功能,能為我們找出具體的類型。
注意,如果調(diào)用f()時傳入了基本數(shù)據(jù)類型,自動打包機制將會被觸發(fā),將基本數(shù)據(jù)類型包裝為對應的對象。
擦除
Java泛型是使用擦除來實現(xiàn)的,這意味著在泛型代碼內(nèi)部,無法獲得關于類型參數(shù)的信息。
謹記,泛型類型參數(shù)將擦除到它的第一個邊界,默認邊界為Object;對于<T extends Bound>,第一個邊界為Bound,即像是在類的聲明中使用Bound替換掉T一樣。
以下例子說明了這一問題:
//: generics/ErasedTypeEquivalence.java
import java.util.*;
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
} /* Output:
true
*///:~
盡管運行時指定了不同的泛型參數(shù),但"ArrayList<String>"與"ArrayList<Integer>"事實上卻被擦除成了相同的原生類型“ArrayList”來進行處理;用類字面常量來進行說明應該會更為直觀:“c1”與"c2"的值為“ArrayList.class”,而不是“ArrayList<String>.class”與“ArrayList<Integer>.class”。
知道了這一點后,你或許能猜測出容器類的一些具體實現(xiàn)細節(jié)了。
打開ArrayList的源碼,會發(fā)現(xiàn)在其內(nèi)部,用來存儲數(shù)據(jù)的數(shù)組是這樣定義的:
/**
* The elements in this list, followed by nulls.
*/
transient Object[] array;
而其get()方法則是這樣:
@SuppressWarnings("unchecked") @Override public E get(int index) {
if (index >= size) {
throwIndexOutOfBoundsException(index, size);
}
return (E) array[index];
}
注意,當E的第一個邊界為Object時,那么這個方法實際上就根本沒有進行轉型(從Object到Object)。
知道了這一點后,你大概會對以下代碼為何能符合預期地運行感到疑惑:
//: generics/GenericHolder.java
public class GenericHolder<T> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
public static void main(String[] args) {
GenericHolder<String> holder =
new GenericHolder<String>();
holder.set("Item");
String s = holder.get(); // Why it works?
}
} ///:~
使用 javap -c 反編譯,我們可以找到答案:
public void set(java.lang.Object);
0: aload_0
1: aload_1
2: putfield #2; //Field obj:Object;
5: return
public java.lang.Object get();
0: aload_0
1: getfield #2; //Field obj:Object;
4: areturn
public static void main(java.lang.String[]);
0: new #3; //class GenericHolder
3: dup
4: invokespecial #4; //Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5; //String Item
11: invokevirtual #6; //Method set:(Object;)V
14: aload_1
15: invokevirtual #7; //Method get:()Object;
18: checkcast #8; //class java/lang/String --------Watch this line--------
21: astore_2
22: return
奧秘就是,編譯器在編譯期為我們執(zhí)行類型檢查,然后插入了轉型代碼。
再看下面這個例子:
//: generics/ArrayMaker.java
import java.lang.reflect.*;
import java.util.*;
public class ArrayMaker<T> {
private Class<T> kind;
public ArrayMaker(Class<T> kind) { this.kind = kind; }
@SuppressWarnings("unchecked")
T[] create(int size) {
return (T[])Array.newInstance(kind, size);
}
public static void main(String[] args) {
ArrayMaker<String> stringMaker =
new ArrayMaker<String>(String.class);
String[] stringArray = stringMaker.create(9);
System.out.println(Arrays.toString(stringArray));
}
} /* Output:
[null, null, null, null, null, null, null, null, null]
*///:~
因為擦除的關系,kind只是被存儲為Class,使用“Array.newInstance();”創(chuàng)建數(shù)組也就只能得到非具體的結果,實際使用中我們需要對其進行向下轉型,但是并沒有足夠的類型信息用以進行類型檢查,所以對于編譯器報錯,只能采用注解“@SuppressWarnings("unchecked")”強行將其消去。
通配符
有些時候你需要限定條件,使用通配符可以滿足這一特性。
這是指定上界的情況:
//: generics/GenericsAndCovariance.java
import java.util.*;
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>,讀作“任何從Fruit繼承而來的類型構成的列表”。但這并不意味著這個List將持有任何類型的Fruit,通配符引用的其實是明確的類型,這個例子中它意味著“某種指定了上界為Fruit的具體類型”。
造成flist的add()完全不可用的原因是,在這種情況下add()的參數(shù)也變成了“? extends Fruit”。下面這個例子可以幫助你進行理解:
//: generics/Holder.java
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
*///:~
同樣的道理,對于上例中的fruit來說,其set()方法的參數(shù)變成了“? extends Fruit”,這意味著其接受的參數(shù)可以是任意類型,只需滿足上界為Fruit即可,而編譯器無法驗證“任意類型”的類型安全性。
反過來看看指定下界的效果:
//: generics/SuperTypeWildcards.java
import java.util.*;
class Jonathan extends Apple {}
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
} ///:~
可以看到,寫入操作變得合法。顯然,Apple類型滿足下界需求,執(zhí)行寫入操作沒有安全性問題,而Jonathan是Apple的子類,經(jīng)過向上轉型,也可以符合需求,而Apple的基類Fruit則仍然由于類型不定而被拒絕。
基本類型不能作為類型參數(shù)
不能創(chuàng)建List<int>之類,而需使用List<Integer>,但因為自動包裝機制的存在,所以寫入數(shù)據(jù)時可以使用基本數(shù)據(jù)類型。
實現(xiàn)參數(shù)化接口
一個類不能實現(xiàn)同一個泛型接口的兩種變體,因為擦除會讓它們變成相同的接口:
//: generics/MultipleInterfaceVariants.java
// {CompileTimeError} (Won’t compile)
interface Payable<T> {}
class Employee implements Payable<Employee> {}
class Hourly extends Employee
implements Payable<Hourly> {} ///:~
Hourly不能編譯。但是,如果從Payable的兩種用法中移除掉泛型參數(shù)(就像編譯器在擦除階段做的那樣),這段代碼將能夠編譯。
重載
以下代碼無法編譯,因為擦除會讓兩個方法產(chǎn)生相同的簽名:
//: generics/UseList.java
// {CompileTimeError} (Won’t compile)
import java.util.*;
public class UseList<W,T> {
void f(List<T> v) {}
void f(List<W> v) {}
} ///:~
自限定類型
class SelfBounded<T extends SelfBounded<T>> { // ...
待補充...
參考資料
- 《Java編程思想》