帶着問題閱讀html
一、什麼是Java泛型,有什麼用處java
二、Java泛型的實現機制是什麼編程
三、Java泛型有哪些侷限和限制數組
引入泛型以前,試想編寫一個加法器,爲處理不一樣數字類型,就須要對不一樣類型參數進行重載,但其實現內容是徹底同樣的,若是是一個更復雜的方法,無疑會形成重複。ide
public int add(int a, int b) {return a + b;} public float add(float a, float b) {return a + b;} public double add(double a, double b) {return a + b;}
通常的類和方法,只能使用具體的類型,要麼是基本類型,要麼是自定義的類。若是要編寫能夠應用於多種類型的代碼,這種刻板的限制對代碼的束縛就會很大。《Java編程思想》工具
Java在1.5版本引入泛型,經過泛型實現的加法代碼可簡化爲:編碼
public <T extends Number> double add(T a, T b) { return a.doubleValue() + b.doubleValue(); }
泛型的核心概念是參數化類型,使用參數指定方法類型,而非硬編碼。泛型的出現帶給咱們不少好處,其中最重要的莫過於對集合類的改進,避免了任意類型均可以丟到同一個集合裏的不可靠問題。設計
然而Python和Go的集合能夠容納任意類型,這到底是進步仍是退步呢code
泛型通常有三種使用方式:泛型類、泛型接口和泛型方法。htm
public class GenericClass<T> { private T member; } ... // 初始化時指定泛型類型 GenericClass<String> instance = new GenericClass<String>();
public interface GenericInterface<T> { void test(T param); } // 實現類指定泛型類型 public class GenericClass implements GenericInterface<String> { @Override public void test(String param) {...} }
如前文中加法代碼的實現就是泛型方法。
// 在方法前添加<T>,泛型類型可用於返回值也可用於參數 public <T> T function(T param); ... function("123"); // 編譯器自動識別T爲String
List<String> strList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); System.out.println(strList.getClass() == intList.getClass()); //true
對如上部分代碼,相信多數人接觸到泛型的第一時刻都認爲這是兩個不一樣的類型,反編譯其字節碼得到代碼以下:
ArrayList var1 = new ArrayList(); ArrayList var2 = new ArrayList(); System.out.println(var1.getClass() == var2.getClass());
咱們發現兩個列表都變成ArrayList類型,若是你們對Jdk1.5以前的版本還有印象就能夠看出,這一段反編譯的代碼就是Java集合最初的使用形式。所以,Java泛型的是經過編譯期將泛型的實際類型擦除爲原始類型(一般爲Object)實現的僞泛型。
所謂僞泛型,是相對C++的"真泛型"(異構擴展,可見參考第三條),在Java中,因爲編譯後擦除了具體類型,在泛型代碼內部,沒法得到任何有關泛型參數類型的信息,在運行期代碼所持有的也只是擦除後的原始類型,也就意味着在運行期能夠經過反射的方式爲泛型類傳入任何原始類型的參數。
public class GenericTest { public List<Integer> ints = new ArrayList<>(); public static void main(String[] args) { GenericTest test = new GenericTest(); List<GenericTest> list = (List<GenericTest>) GenericTest.class.getDeclaredField("ints").get(test); list.add(new GenericTest()); System.out.println(test.ints.get(0)); // 打印GenericTest變量地址 int number = test.ints.get(0); // 類型轉換拋出異常 } } // 泛型代碼內部是指泛型類或泛型方法內部。 public class Generic<T> { public Class getTClass() { //沒法獲取 } } public <T> Class getParamClass(T param) { //沒法獲取 }
在泛型外部能夠獲取已指定的泛型參數類型,經過javap -v
查看Constant Pool
,可看到具體類型記錄在Signature
。
public class Outer { private List<String> list = new ArrayList<>(); //能夠獲取list的具體類型 }
事實上在Java推出泛型時,C++的模板泛型已經至關成熟,設計者也並不是沒有能力實現包含具體類型的泛型,使用類型擦除最重要的緣由仍是爲了保持兼容性。假設ArrayList<String>
和ArrayList
編譯後是不一樣的class,那麼爲了兼容舊代碼正常運行,必須平行的添加一套泛型集合並在後續版本中同時維護,而集合類做爲大量使用的基礎工具類,開發者不得不爲此承擔大量代碼切換的風險(參考Vector
和HashTable
的帶來的遺留問題),所以相較於兼容性的取捨,採用類型擦除實現泛型算是折中方案。
思考一下,下面的類能夠編譯經過嗎
public class Test { void test(List<String> param) {} void test(List<Integer> param) {} }
前面說到泛型會被擦除爲原始類型,通常是Object
。若是泛型聲明爲<? extends Number>
,就會被擦除爲Number
。
List<Number> numbers = new ArrayList<>(); List<Integer> integers = new ArrayList<>(); numbers = integers; // compile error
考慮以上代碼,numbers
能夠增長Integer
類型的元素,直覺上integers
應該也能夠賦值給numbers
。因爲類型擦除,Java在編譯期限定了只有相同類型的泛型實例才能夠互相賦值,但這樣就違背了Java的多態,爲了解決泛型轉換的問題,Java引入了上下限<? extends A>
和<? super B>
兩種機制。
若是泛型聲明爲<? extends A>
,即聲明該泛型的上界也即擦除後的原始類型爲A
,同時該泛型類的實例能夠引用A
子類的泛型實例。
// 上界保證取出來的元素必定是Number,但沒法約束放入的類型 List<Integer> integers = new ArrayList<>(); List<Float> floats = new ArrayList<>(); List<? extends Number> numbers = integers; // numbers = floats; 也能夠 numbers.get(0); // ok,總能保證取出的必定是Number numbers.put(1); // compile error,沒法保證放入的是否符合約束
若是泛型聲明爲<? super B>
,即聲明該泛型的下界爲B
,原始類型仍爲Object
,同時該泛型類的實力能夠引用B
父類的泛型實例。
// 假設三個繼承類 Child -> Father -> GrandFather // 下界保證寫入的元素必定是Child,但沒法肯定取出的類型 List<Father> fathers = new ArrayList<>(); List<GrandFather> grandFathers = new ArrayList<>(); List<? super Child> childs = fathers; // childs = grandFathers; 也能夠 numbers.put(new Child()); //ok,總能保證明際容器可接受Child Child ele = (Child) numbers.get(0); // runtime error,沒法肯定獲得的具體類型
在Java中,根據裏式替換原則,向上轉型是默認合法的,向下轉型則須要強制轉換,如不能轉換則報錯。在extends
的get
和super
的put
場景中,必定能夠保證讀取/放入的元素是能夠向上轉型的,而在extends
的put
和super
的get
中,則沒法確承認轉的類型,所以extends
只能讀取,super
只能寫入。
固然若是使用super時,取出的對象以Object存放,也沒有問題,由於super擦除後的原始類型爲Object。
參考《Effective Java》中給出的PECS
使用建議。
爲了得到最大限度的靈活性,要在表示生產者或消費者的輸入參數上使用通配符類型。
若是參數化類型表示一個T生產者,就使用<? extends T>。 producer-extends
若是參數化類型表示一個T消費者,就使用<? super T>。consumer-super
若是某個輸入參數便是生產者又是消費者,那麼通配符類型對你就沒什麼好處了。
這一段話筆者認爲有必定迷惑性,生產者是寫入的,消費者是讀取的,前文介紹過extends
用於讀取,而super
用於寫入,偏偏相反。
我的認爲對這段話的正確理解是以泛型爲第一視角切入,即當泛型類型自己做爲生產者提供功能(被讀取)時使用extends
,反之(被寫入)使用super
。而很是規意義上生產者要寫入的容器採用extends
,消費者讀取的容器使用super
。
// producer,此時返回值做爲生產後的結果提供給消費者 List<? extends A> writeBuffer(...); // consumer,此時返回值做爲消費後的結果提供給生產者 List<? super B> readBuffer(...);
泛型類也能夠被繼承,泛型類主要有兩種繼承方式。
public class Father<T> { public void test(T param){} } // 泛型繼承,Child依然是泛型類 public class Child<T> extends Father<T> { @Override public void test(T param){} } // 指定泛型類型,StringChild爲具體類 public class StringChild extends Father<String> { @Override public void test(String param){} }
咱們知道@Override
是保持簽名不變且重寫父類方法,查看Father
類字節碼,其中test方法被擦除爲void test(Object param)
;在StringChild
中,方法簽名爲void test(String param)
。到此讀者可能意識到,這根本不是重寫而是重載(Overload
)。
查看StringChild
的字節碼。
... #3 = Methodref ... public void test(java.lang.String); ... invokespecial #3 // Method StringChild.test:(Ljava/lang.Object;) V ... public void test(java.lang.Object);
能夠看到其中實際包含了兩個方法,一個參數是String
一個是Object
,後者纔是對父類方法的重寫,前者經過invoke轉到對後者的調用。這個方法是JVM在編譯時自動添加的,也叫作橋方法。同時還有一點須要說起,示例中的代碼是以泛型參數做爲入參,做爲返回類型的話會產生Object test()
和String test()
兩個方法,這兩個方法在常規的編碼中是沒法編譯經過的,但JVM爲泛型多態的實現容許了這個不合規的存在。
Object
,所以範型不支持基本類型List<int> intList = new ArrayList<>(); // 沒法編譯
T instance = new T(); // 不能直接使用泛型初始化 if (t instanceOf T); // 不能判斷泛型類型 T[] array = new T[]{}; // 不能建立泛型數組
// Error public class Generic<T> { public static T t; public static T test() {return t;} }
// 假設繼承實現一個泛型異常 class SomeException<T> extends Exception... try { ... } catch(SomeException<Integer> | SomeException<String> ex) { //因爲類型擦除,沒法捕獲多個泛型異常 ... }