在程序日益複雜龐大的今天,編寫泛用性代碼的價值愈發變得巨大。
而要作到這一點,其訣竅僅只兩字而已————解耦。java
最簡單的解耦,無疑是使用基類替代子類。然而因爲 Java 僅支持單繼承,這種解耦方法所帶來的侷限性未免過大,有種「只准投胎一次」的感受。數組
使用接口替代具體類算是更近了一步,算是多給了一條命吧,但限制依舊存在。要是咱們所寫的代碼自己就是爲了應用於「某種不肯定的類型」呢?安全
這時候就輪到泛型登場了。app
雖然理想遠大。但 Java 引入泛型的初衷,也許只是爲了建立容器類也說不定。ide
站在類庫設計者的角度,咱們不妨走上一遭。ui
得益於單根繼承結構,咱們能夠這樣來設計一個持有單個對象的容器:this
public class Holder1 { private Object a; public Holder1(Object a) { this.a = a; } Object get() { return a; } }
這個容器確實能持有多種類型的對象,但一般而言咱們只會用它來存儲一種對象。也就是說雖然設計時但願它能存儲任意類型,但使用時卻只能存儲咱們想要的肯定類型。url
泛型能夠達到這一目的,與此同時,這也能使編譯器爲咱們提供編譯器檢查。設計
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 } }
如你所見,使用方法即在類名後添加尖括號,而後填寫類型參數 「T」。使用時用明確的類型參數替換掉「T」,即爲該容器指定了其存儲的「肯定類型」。rest
泛型能夠應用於方法,只須要將泛型參數列表放在方法返回值以前便可。
下面這個例子中,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 *///:~
能這樣作的緣由在於編譯器擁有稱爲類型參數推斷的功能,能爲咱們找出具體的類型。
注:若是調用 f() 時傳入了基本數據類型,自動打包機制將會被觸發,將基本數據類型包裝爲對應的對象。
Java 泛型是使用擦除來實現的,這意味着在泛型代碼內部,沒法獲取關於類型參數的信息。
謹記,泛型類型參數將擦除到它的第一個邊界,默認邊界爲 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 *///:~
儘管運行時指定了不一樣的泛型參數,但 ArrayList<String> 和 ArrayList<Integer> 事實上卻被擦除成立相同的原生類型 ArrayList 來進行處理;用類字面常量來進行說明應該會更爲直觀:c1 與 c2 的值爲 ArrayList.class,而不是 ArrayList<String>.class 與 ArrayList<Integer>.class。
知道了這一點後,你或許能猜想出容器類的一些具體實現細節了。
打開 ArrayList 的源碼,會發如今其內部,用來存儲數據的數組都是這樣定義的:
/** * 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
奧祕就是,編譯器在編譯期爲咱們執行類型檢查,而後插入了轉型代碼。
再看下面這個例子:
//: 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(); 建立數組也就只能獲得非具體的結果,實際使用中咱們須要對其進行向下轉型,可是並無足夠的類型信息用以進行類型檢查,因此編譯器報錯,只能採用註解 @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() 的參數也變成了 」? 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 *///:~
一樣的道理,對於上例中的 flist 來講,其 set() 方法的參數變成了 "? extends Fruit",這意味着其接受的參數能夠時任意類型,只需知足上界爲 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 類型知足下界需求,執行寫入操做沒有安全性問題,而 Jonathan 時 Apple 的子類,通過向上轉型,也能夠符合要求,而 Apple 的基類 Fruit 則仍然因爲類型不定而被拒絕。
不能建立 List<int> 之類,而需使用 List<Integer>,但由於自動包裝機制的存在,因此寫入數據時可使用基本數據類型。
一個類不能實現同一個泛型接口的兩種變體,由於擦除會讓它們變成相同的接口:
//: generics/MultipleInterfaceVariants.java // {CompileTimeError} (Won’t compile) interface Payable<T> {} class Employee implements Payable<Employee> {} class Hourly extends Employee implements Payable<Hourly> {} ///:~
Hourly 不能編譯。可是,若是從 Payable 的兩種用法中移除掉泛型參數(就像編譯器在擦除階段作的那樣),這段代碼將可以編譯。
重載 如下代碼沒法編譯,由於擦除會讓兩個方法產生相同的簽名:
//: 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) {} } ///:~