泛型是Java基礎知識的重點,雖然咱們在初學Java的時候,都學過泛型,以爲本身掌握對於Java泛型的使用(全是錯覺),日後的日子,當咱們深刻去閱讀一些框架源碼,你就發現了,本身會的只是簡單的使用,卻看不懂別人的泛型代碼是怎麼寫的,還能夠這樣,沒錯,別人寫出來的代碼那叫藝術,而我......html
Java
語言爲何存在着泛型,而像一些動態語言Python
,JavaScipt
卻沒有泛型的概念?java
緣由是,像Java
,C#
這樣的靜態編譯型的語言,它們在傳遞參數的時候,參數的類型,必須是明確的,看一個例子,簡單編寫一個存放int
類型的棧—StackInt
,代碼以下:git
public class StackInt { private int maxSize; private int[] items; private int top; public StackInt(int maxSize){ this.maxSize = maxSize; this.items = new int[maxSize]; this.top = -1; } public boolean isFull(){ return this.top == this.maxSize-1; } public boolean isNull(){ return this.top <= -1; } public boolean push(int value){ if(this.isFull()){ return false; } this.items[++this.top] = value; return true; } public int pop(){ if(this.isNull()){ throw new RuntimeException("當前棧中無數據"); } int value = this.items[top]; --top; return value; } }
在這裏使用構造函數初始化一個StackInt
對象時,能夠傳入String
字符串嗎?很明顯是不行的,咱們要求的是int
類型,傳入字符串String
類型,這樣在語法檢查階段時會報錯的,像Java
這樣的靜態編譯型的語言,參數的類型要求是明確的github
參數不安全:引入泛型,可以在編譯階段找出代碼的問題,而不是在運行階段編程
泛型要求在聲明時指定實際數據類型,Java 編譯器
在編譯時會對泛型代碼作強類型檢查,並在代碼違反類型安全時發出告警。早發現,早治理,把隱患扼殺於搖籃,在編譯時發現並修復錯誤所付出的代價遠比在運行時小。數組
避免類型轉換:安全
未使用泛型:oracle
List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0); //須要在取出Value的時候進行強制轉換
使用泛型:app
List<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); //不須要強制轉換
重複編碼::經過使用泛型,能夠實現通用編碼,能夠處理不一樣類型的集合,而且類型安全且易於閱讀。像上面的StackInt
類,咱們不能針對每一個類型去編寫對應類型的棧,那樣太麻煩了,而泛型的出現就很好的解決了這點框架
在上面的StackInt
類有一些很差的地方,那就是太具體了,不夠抽象,不夠抽象,那麼它的複用性也是不高的,例如,在另外的場景下,我須要的是往棧裏存String
類型的字符串,或者是其餘類型,那麼StackInt
類就作不到了,那麼有什麼方法可以作到呢?再寫一個StackString
類,不可能,那樣不得累死。那就只有引入基類Object
了,咱們改進一下代碼:
public class StackObject { private int maxSize; private Object[] items; private int top; public StackObject(int maxSize){ this.maxSize = maxSize; this.items = new Object[maxSize]; this.top = -1; } public boolean isFull(){ return this.top == this.maxSize-1; } public boolean isNull(){ return this.top <= -1; } public boolean push(Object value){ if(this.isFull()){ return false; } this.items[++this.top] = value; return true; } public Object pop(){ if(this.isNull()){ throw new RuntimeException("當前棧中無數據"); } Object value = this.items[top]; --top; return value; } }
使用StackObject
能夠存儲任意類型的數據,那麼這樣作,又有什麼優勢和缺點呢?
優勢:StackObject
類變得相對抽象了,咱們能夠往裏面存儲任何類型的數據,這樣就避免了寫一些重複代碼
缺點:
一、用Object
表示的對象是比較抽象的,它失去了類型的特色,那麼咱們在作一些運算的時候,可能會頻繁的拆箱裝箱的過程
看上面的例圖,咱們理解的認爲存放了兩個數值,12345
和54321
,將兩個進行相加,這是很常見的操做,可是報錯了,編譯器給咱們的提示是,+
操做運算不能用於兩個Object
類型,那麼只能對其進行類型轉換,這也是咱們上面說到的泛型能解決的問題,咱們須要這樣作,int sum = (int)val1 + (int)val2;
,同時在涉及拆箱裝箱時,是有必定性能的損耗的,關於拆箱裝箱
在這裏不做描述,能夠參考我寫過的隨筆—— 深刻理解Java之裝箱與拆箱
二、對於咱們push
進去的值,咱們在取出的時候,容易忘記類型轉換,或者不記得它的類型,類型轉換錯誤,這在後面的一些業務可能埋下禍根,例以下面這個場景:直到運行時錯誤才暴露出來,這是不安全的,也是違反軟件開發原則的,應該儘早的在編譯階段就發現問題,解決問題
三、使用Object
太過於模糊了,沒有具體類型的意義
最好不要用到Object
,由於Object
是一切類型的基類,也就是說他把一些類型的特色給抹除了,好比上面存的數字,對於數字來講,加法運算就是它的一個特色,可是用了Object
,它就失去了這一特色,失去類型特有的行爲
泛型:是被參數化的類或接口,是對類型的約定
class name<T1, T2, ..., Tn> { /* ... */ }
通常將泛型中的類名稱爲原型,而將 <>
指定的參數稱爲類型參數,<>
至關於類型的約定,T
就是類型,至關於一個佔位符,由咱們在調用時指定
使用泛型改進一下上面StackObject
類,可是,數組和泛型不能很好地結合。你不能實例化具備參數化類型的數組,例以下面的代碼是不合格的:
public StackT(int maxSize){ this.maxSize = maxSize; this.items = new T[maxSize]; this.top = -1; }
Java
中不容許直接建立泛型數組,這是由於相比於C++
,C#
的語法,Java
泛型實際上是僞泛型,這點在後面會說到,可是,能夠經過建立一個類型擦除的數組,而後轉型的方式來建立泛型數組。
private int maxSize; private T[] items; private int top; public StackT(int maxSize){ this.maxSize = maxSize; this.items = (T[]) new Object[maxSize]; this.top = -1; }
實際上,真的須要存儲泛型,仍是使用容器更合適,回到原來的代碼上,須要知道的是,泛型類型不能是基本類型的,須要是包裝類
上面說到了Java
中不容許直接建立泛型數組,事實上,Java
中的泛型咱們是很難通new
的方式去實例化對象,不只僅是實例化對象,甚至是獲取T
的真實類型也是很難的,固然經過反射的機制仍是能夠獲取到的,Java
獲取真實類型的方式有 3 種,分別是:
一、類名.class
二、對象.getClass
三、class.forName("全限定類名")
可是,在這裏,1
和2
的方式都是作不到的,雖然咱們在外邊明確的傳入了Integer
類型,new StackT<Integer>(3);
可是在StackT
類,使用T.class
仍是獲取不到真實類型的,第 2 種方式的話,並無傳入對象,前面也說到是沒有辦法new
方式實例化的,而經過反射機制是能夠作到的,這裏不做演示,須要瞭解的話能夠參考 —— Java如何得到泛型類的真實類型、 Java經過反射獲取泛型的類型
可是在C#
中的泛型以及C++
的模板,這是很容易作到的,因此說Java
的泛型是僞泛型,Java
並非作不到像C#
同樣,而是爲了遷就老的JDK
語法所做出的妥協,至於上面爲何作不到這樣,這就要說到泛型的類型擦除了。
再說類型擦除以前,先說一下泛型接口,和泛型方法吧
接口也能夠聲明泛型,泛型接口語法形式:
public interface Content<T> { T text(); }
泛型接口有兩種實現方式:
public class ContentImpl implements Content<Integer> { private int text; public ContentImpl(int text) { this.text = text; } public static void main(String[] args) { ContentImpl one = new ContentImpl(10); System.out.print(one.text()); } } // Output: // 10
public class ContentImpl<T> implements Content<T> { private T text; public ContentImpl(T text) { this.text = text; } @Override public T text() { return text; } public static void main(String[] args) { ContentImpl<String> two = new ContentImpl<>("ABC"); System.out.print(two.text()); } } // Output: // ABC
泛型方法是引入其本身的類型參數的方法。泛型方法能夠是普通方法、靜態方法以及構造方法。
泛型方法語法形式以下:
public <T> T func(T obj) {}
是否擁有泛型方法,與其所在的類是不是泛型沒有關係。
泛型方法的語法包括一個類型參數列表,在尖括號內,它出如今方法的返回類型以前。對於靜態泛型方法,類型參數部分必須出如今方法的返回類型以前。類型參數能被用來聲明返回值類型,而且能做爲泛型方法獲得的實際類型參數的佔位符。
使用泛型方法的時候,一般沒必要指明類型參數,由於編譯器會爲咱們找出具體的類型。這稱爲類型參數推斷(type argument inference)。類型推斷只對賦值操做有效,其餘時候並不起做用。若是將一個泛型方法調用的結果做爲參數,傳遞給另外一個方法,這時編譯器並不會執行推斷。編譯器會認爲:調用泛型方法後,其返回值被賦給一個 Object 類型的變量。
public class GenericsMethod { public static <T> void printClass(T obj) { System.out.println(obj.getClass().toString()); } public static void main(String[] args) { printClass("abc"); printClass(10); } } // Output: // class java.lang.String // class java.lang.Integer
泛型方法中也可使用可變參數列表
public class GenericVarargsMethod { public static <T> List<T> makeList(T... args) { List<T> result = new ArrayList<T>(); Collections.addAll(result, args); return result; } public static void main(String[] args) { List<String> ls = makeList("A"); System.out.println(ls); ls = makeList("A", "B", "C"); System.out.println(ls); } } // Output: // [A] // [A, B, C]
事實上,Java的運行大體能夠分爲兩個階段,編譯階段
,運行階段
那麼對於Java
泛型來講,當編譯階段事後,泛型 T 是已經被擦除了,因此在運行階段,它已經丟失了 T 的具體信息,而咱們去實例化一個對象的時候,好比T c = new T();
,它的發生時機是在運行階段,而在運行階段,你要new T()
,就須要知道 T 的具體類型,實際上這時候 T
是被替換成Integer
了,而JVM
是不知道T
的類型的,因此是沒有辦法實例化的。
那麼,類型擦除作了什麼呢?它作了如下工做:
<>
的內容。好比 T get()
方法聲明就變成了 Object get()
;List<String>
就變成了 List
。若有必要,插入類型轉換以保持類型安全。讓咱們來看一個示例:
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<String>
中放入一個 Integer
,所獲得的行爲(失敗)和 向 ArrayList<Integer>
中放入一個 Integer
所獲得的行爲(成功)徹底不一樣。可是結果輸出的是true
,這意味着使用泛型時,任何具體的類型信息都被擦除了,ArrayList<Object>
和 ArrayList<Integer>
在運行時,JVM 將它們視爲同一類型class java.util.ArrayList
再用一個例子來對於該謎題的補充:
import java.util.*; class Frob {} class Fnorkle {} class Quark<Q> {} class Particle<POSITION, MOMENTUM> {} public class LostInformation { public static void main(String[] args) { List<Frob> list = new ArrayList<>(); Map<Frob, Fnorkle> map = new HashMap<>(); Quark<Fnorkle> quark = new Quark<>(); Particle<Long, Double> p = new Particle<>(); System.out.println(Arrays.toString(list.getClass().getTypeParameters())); System.out.println(Arrays.toString(map.getClass().getTypeParameters())); System.out.println(Arrays.toString(quark.getClass().getTypeParameters())); System.out.println(Arrays.toString(p.getClass().getTypeParameters())); } } /* Output: [E] [K,V] [Q] [POSITION,MOMENTUM] */
根據 JDK 文檔,Class.getTypeParameters() 「返回一個 TypeVariable 對象數組,表示泛型聲明中聲明的類型參數...」 這暗示你能夠發現這些參數類型。可是正如上例中輸出所示,你只能看到用做參數佔位符的標識符,這並不是有用的信息。
殘酷的現實是:在泛型代碼內部,沒法獲取任何有關泛型參數類型的信息。
以上兩個例子皆出《Java 編程思想》第五版 —— On Java 8
中的例子,本文藉助該例子,試圖講清楚Java
泛型是使用類型擦除這裏機制實現的,能力不足,有錯誤的地方,還請指正。關於On Java 8
一書,已在github
上開源,並有熱心的夥伴將之翻譯成中文,如今給出閱讀地址,On Java 8
擦除的代價是顯著的。泛型不能用於顯式地引用運行時類型的操做中,例如轉型、instanceof 操做和 new 表達式。由於全部關於參數的類型信息都丟失了,當你在編寫泛型代碼時,必須時刻提醒本身,你只是看起來擁有有關參數的類型信息而已。
考慮以下的代碼段:
class Foo<T> { T var; }
看上去當你建立一個 Foo 實例時:
Foo<Cat> f = new Foo<>();
class Foo 中的代碼應該知道如今工做於 Cat 之上。泛型語法也在強烈暗示整個類中全部 T 出現的地方都被替換,就像在 C++ 中同樣。可是事實並不是如此,當你在編寫這個類的代碼時,必須提醒本身:「不,這只是一個 Object「。
繼承問題
泛型時基於類型擦除實現的,因此,泛型類型沒法向上轉型。
向上轉型是指用子類實例去初始化父類,這是面向對象中多態的重要表現。
Integer
繼承了 Object
;ArrayList
繼承了 List
;可是 List<Interger>
卻並不是繼承了 List<Object>
。
這是由於,泛型類並無本身獨有的 Class
類對象。好比:並不存在 List<Object>.class
或是 List<Interger>.class
,Java 編譯器會將兩者都視爲 List.class
。
如何解決上面所產生的問題:
其實並不必定要經過new
的方式去實例化,咱們能夠經過顯式的傳入源類,一個Class<T> clazz
的對象來補償擦除,例如instanceof 操做,在程序中嘗試使用 instanceof 將會失敗。類型標籤可使用動態 isInstance()
,這樣改進代碼:
public class Improve<T> { //錯誤方法 public boolean f(Object arg) { // error: illegal generic type for instanceof if (arg instanceof T) { return true; } return false; } //改進方法 Class<T> clazz; public Improve(Class<T> clazz) { this.clazz = clazz; } public boolean f(Object arg) { return kind.isInstance(arg); } }
實例化:
試圖在 new T()
是行不通的,部分緣由是因爲擦除,部分緣由是編譯器沒法驗證 T 是否具備默認(無參)構造函數。
Java 中的解決方案是傳入一個工廠對象,並使用該對象建立新實例。方便的工廠對象只是 Class 對象,所以,若是使用類型標記,則可使用 newInstance()
建立該類型的新對象:
class Improve<T> { Class<T> kind; Improve(Class<T> kind) { this.kind = kind; } public T get(){ try { return kind.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } } class Employee { @Override public String toString() { return "Employee"; } } public class InstantiateGenericType { public static void main(String[] args) { Improve<Employee> fe = new Improve<>(Employee.class); System.out.println(fe.get()); } } /* Output: Employee */
經過這樣改進代碼,能夠實現建立對象的實例,可是要注意的是,newInstance();
方法調用無參構造函數的,若是傳入的類型,沒有無參構造的話,是會拋出InstantiationException
異常的。
泛型數組:
泛型數組這部分,咱們在上面說到能夠經過建立一個類型擦除的數組,而後轉型的方式來建立泛型數組,此次咱們能夠經過顯式的傳入源類的方式來編寫StackT
類,解決建立泛型數組的問題,代碼以下:
public class StackT<T> { private int maxSize; private T[] items; private int top; public StackT(int maxSize, Class<T> clazz){ this.maxSize = maxSize; this.items = this.createArray(clazz); this.top = -1; } public boolean isFull(){ return this.top == this.maxSize-1; } public boolean isNull(){ return this.top <= -1; } public boolean push(T value){ if(this.isFull()){ return false; } this.items[++this.top] = value; return true; } public T pop(){ if(this.isNull()){ throw new RuntimeException("當前棧中無數據"); } T value = this.items[top]; --top; return value; } private T[] createArray(Class<T> clazz){ T[] array =(T[])Array.newInstance(clazz, this.maxSize); return array; } }
有時您可能但願限制可在參數化類型中用做類型參數的類型。類型邊界
能夠對泛型的類型參數設置限制條件。例如,對數字進行操做的方法可能只想接受 Number
或其子類的實例。
要聲明有界類型參數,請列出類型參數的名稱,而後是 extends
關鍵字,後跟其限制類或接口。
類型邊界的語法形式以下:
<T extends XXX>
示例:
public class GenericsExtendsDemo01 { static <T extends Comparable<T>> T max(T x, T y, T z) { T max = x; // 假設x是初始最大值 if (y.compareTo(max) > 0) { max = y; //y 更大 } if (z.compareTo(max) > 0) { max = z; // 如今 z 更大 } return max; // 返回最大對象 } public static void main(String[] args) { System.out.println(max(3, 4, 5)); System.out.println(max(6.6, 8.8, 7.7)); System.out.println(max("pear", "apple", "orange")); } } // Output: // 5 // 8.8 // pear
示例說明:
上面的示例聲明瞭一個泛型方法,類型參數
T extends Comparable<T>
代表傳入方法中的類型必須實現了 Comparable 接口。
類型邊界能夠設置多個,語法形式以下:
<T extends B1 & B2 & B3>
注意:extends 關鍵字後面的第一個類型參數能夠是類或接口,其餘類型參數只能是接口。
通配符是Java
泛型中的一個很是重要的知識點。不少時候,咱們其實不是很理解通配符?
和泛型類型T
區別,容易混淆在一塊兒,其實仍是很好理解的,?
和 T
都表示不肯定的類型,區別在於咱們能夠對 T
進行操做,可是對 ?
不行,好比以下這種 :
// 能夠 T t = operate(); // 不能夠 ? car = operate();
可是這個並非咱們混淆的緣由,雖然?
和 T
都表示不肯定的類型,T
一般用於泛型類和泛型方法的定義,?
一般用於泛型方法的調用代碼和形參,不能用於定義類和泛型方法。用代碼解釋一下,回到文章最初說的棧類StackT
,咱們以這個爲基礎來解釋,上面的觀點:
public class Why { public static void main(String[] args) { StackT<Integer> stackT = new StackT<>(3, Integer.class); stackT.push(8); StackT<String> stackT1 = new StackT<>(3, String.class); stackT1.push("7"); test(stackT1); } public static void test(StackT stackT){ System.out.println(stackT.pop()); } } // Output: 8
以咱們編寫的StackT
類,進行測試,編寫一個test
方法,傳入參數類型StackT
,上面的程序正常輸出字符串"7" ,這沒有什麼問題,問題在這裏失去了泛型的限定,傳進去的實參StackT1
,是被咱們限定爲StackT<String>
,可是咱們經過編譯器能夠看到stackT.pop()
出來的對象,並無String
類型的特有方法,也就是說,它實際上是Object
類
那麼咱們就須要修改test
方法的形參,改成:
public static void test(StackT<String> stackT){ System.out.println(stackT.pop()); }
這樣子就回到了咱們問題的本質來了,將形參修改成StackT<String>
,這起到了泛型的限定做用,可是會出現這樣的問題,若是咱們須要向該方法傳入StackT<Integer>
類型的對象 stackT
是,由於方法形參限定了StackT<String>
,,這時候就報錯了
這個時候就是通配符?
起做用了,將方法形參改成StackT<?>
就能夠了,這也就肯定了咱們剛剛的結論,?
通配符一般是用於泛型傳參,而不是泛型類的定義。
public static void test(StackT<?> stackT){ System.out.println(stackT.pop()); }
可是這種用法咱們一般也不會去用,由於它仍是失去了類型的特色,即當無界泛型通配符做爲形參時,做爲調用方,並不限定傳遞的實際參數類型。可是,在方法內部,泛型類的參數和返回值爲泛型的方法,不能使用!
這裏,StackT.push
就不能用了,由於我並不知道?
傳的是Integer
仍是String
,仍是其餘類型,因此是會報錯的。
可是咱們有時候是有這樣的需求的,咱們在接收泛型棧StackT
做爲形參的時候,我想表達一種約束的關係,可是又不像StackT<String>
同樣,約束的比較死板,而Java
是面向對象的語言,那麼就會有繼承的機制,我想要的約束關係是我能接收的泛型棧的類型都是Number
類的派生類,即不會像?
無界通配符同樣失去類的特徵,又不會像StackT<String>
約束的很死,這就引出了上界通配符的概念。
可使用上界通配符
來縮小類型參數的類型範圍。
它的語法形式爲:<? extends Number>
public class Why { public static void main(String[] args) { StackT<Integer> stackT = new StackT<>(3, Integer.class); stackT.push(8); StackT<String> stackT1 = new StackT<>(3, String.class); stackT1.push("7"); StackT<Double> stackT2 = new StackT<>(3, Double.class); //經過 test(stackT); test(stackT2); //error test(stackT1); } public static void test(StackT<? extends Number> stackT){ System.out.println(stackT.pop()); } }
這樣就實現了一類類型的限定,可是需求變動了,我如今但願的約束關係是我能接收的泛型棧的類型都是Number
類的父類,或者父類的父類,那麼有上界,天然就有下界
下界通配符
將未知類型限制爲該類型的特定類型或超類類型。
注意:上界通配符和下界通配符不能同時使用。
它的語法形式爲:<? super Number>
public class Why { public static void main(String[] args) { StackT<Number> stackT1 = new StackT<>(3, Number.class); stackT1.push(8); StackT<Double> stackT2 = new StackT<>(3, Double.class); StackT<Object> stackT3 = new StackT<>(3, Object.class); //經過 test(stackT1); test(stackT3); //error test(stackT2); } public static void test(StackT<? super Number> stackT){ System.out.println(stackT.pop()); } }
這樣子的話,就確保了咱們的test
方法只接收Number
類型以上的方法。泛型的各類高級語法可能在寫業務代碼的時候能夠規避,可是若是你要去寫一些框架的時候,因爲你不知道框架的使用者的使用場景,那麼掌握泛型的高級語法就頗有用了。
前面,咱們提到:泛型不能向上轉型。可是,咱們能夠經過使用通配符來向上轉型。
public class GenericsWildcardDemo { public static void main(String[] args) { List<Integer> intList = new ArrayList<>(); List<Number> numList = intList; // Error List<? extends Integer> intList2 = new ArrayList<>(); List<? extends Number> numList2 = intList2; // OK } }
通配符邊界問題,關於一些更加深刻的解惑能夠參考整理的轉載的文章——Java泛型解惑之上下通配符
Pair<int, char> p = new Pair<>(8, 'a'); // 編譯錯誤
public static <E> void append(List<E> list) { E elem = new E(); // 編譯錯誤 list.add(elem); }
public class MobileDevice<T> { private static T os; // error // ... }
public static <E> void rtti(List<E> list) { if (list instanceof ArrayList<Integer>) { // 編譯錯誤 // ... } } List<Integer> li = new ArrayList<>(); List<Number> ln = (List<Number>) li; // 編譯錯誤
List<Integer>[] arrayOfLists = new List<Integer>[2]; // 編譯錯誤
// Extends Throwable indirectly class MathException<T> extends Exception { /* ... */ } // 編譯錯誤 // Extends Throwable directly class QueueFullException<T> extends Throwable { /* ... */ // 編譯錯誤 public static <T extends Exception, J> void execute(List<J> jobs) { try { for (J job : jobs) // ... } catch (T e) { // compile-time error // ... } }
public class Example { public void print(Set<String> strSet) { } public void print(Set<Integer> intSet) { } // 編譯錯誤 }
泛型一些約定俗成的命名:
Java泛型解惑之 extends T>和 super T>上下界限
7月的直播課——Java 高級語法—泛型