java 泛型全解 - 絕對最詳細
背景
對於java的泛型我一直屬於一知半解的,平常真心用的不多。直到閱讀《Effect Java》,看到很多平常不瞭解的用法,才下定決心,需要系統的學習,並且記錄下來。
1、泛型的概述:
1.1 泛型的由來
根據《Java程式設計思想》中的描述,泛型出現的動機:
有很多原因促成了泛型的出現,而最引人注意的一個原因,就是為了建立容器類。
複製程式碼
泛型的思想很早就存在,如C++中的模板(Templates)。模板的精神:引數化型別
1.2 基本概述
- 泛型的本質就是"引數化型別"。一提到引數,最熟悉的就是定義方法的時候需要形參,呼叫方法的時候,需要傳遞實參。那"引數化型別"就是將原來具體的型別引數化
- 泛型的出現避免了強轉的操作,在編譯器完成型別轉化,也就避免了執行的錯誤。
1.3 泛型的目的
- Java泛型也是一種語法糖,在編譯階段完成型別的轉換的工作,避免在執行時強制型別轉換而出現ClassCastException,型別轉化異常。
1.4 例項
JDK 1.5時增加了泛型,在很大的程度上方便在集合上的使用。
- 不使用泛型:
public static void main(String[] args) {
List list = new ArrayList();
list.add(11);
list.add("ssss");
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
複製程式碼
因為list型別是Object。所以int,String型別的資料都是可以放入的,也是都可以取出的。但是上述的程式碼,執行的時候就會丟擲型別轉化異常,這個相信大家都能明白。
- 使用泛型:
public static void main(String[] args) {
List<String> list = new ArrayList();
list.add("hahah" );
list.add("ssss");
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
複製程式碼
在上述的例項中,我們只能新增String型別的資料,否則編譯器會報錯。
2、泛型的使用
泛型的三種使用方式:泛型類,泛型方法,泛型介面
2.1 泛型類
- 泛型類概述:把泛型定義在類上
- 定義格式:
public class 類名 <泛型型別1,...> {
}
複製程式碼
- 注意事項:泛型型別必須是引用型別(非基本資料型別)
2.2 泛型方法
- 泛型方法概述:把泛型定義在方法上
- 定義格式:
public <泛型型別> 返回型別 方法名(泛型型別 變數名) {
}
複製程式碼
- 注意要點:
- 方法宣告中定義的形參只能在該方法裡使用,而介面、類宣告中定義的型別形參則可以在整個介面、類中使用。當呼叫fun()方法時,根據傳入的實際物件,編譯器就會判斷出型別形參T所代表的實際型別。
class Demo{
public <T> T fun(T t){ // 可以接收任意型別的資料
return t ; // 直接把引數返回
}
};
public class GenericsDemo26{
public static void main(String args[]){
Demo d = new Demo() ; // 例項化Demo物件
String str = d.fun("湯姆") ; // 傳遞字串
int i = d.fun(30) ; // 傳遞數字,自動裝箱
System.out.println(str) ; // 輸出內容
System.out.println(i) ; // 輸出內容
}
};
複製程式碼
2.3 泛型介面
- 泛型介面概述:把泛型定義在介面
- 定義格式:
public interface 介面名<泛型型別> {
}
複製程式碼
- 例項:
/**
* 泛型介面的定義格式: 修飾符 interface 介面名<資料型別> {}
*/
public interface Inter<T> {
public abstract void show(T t) ;
}
/**
* 子類是泛型類
*/
public class InterImpl<E> implements Inter<E> {
@Override
public void show(E t) {
System.out.println(t);
}
}
Inter<String> inter = new InterImpl<String>() ;
inter.show("hello") ;
複製程式碼
2.4 原始碼中泛型的使用,下面是List介面和ArrayList類的程式碼片段。
//定義介面時指定了一個型別形參,該形參名為E
public interface List<E> extends Collection<E> {
//在該介面裡,E可以作為型別使用
public E get(int index) {}
public void add(E e) {}
}
//定義類時指定了一個型別形參,該形參名為E
public class ArrayList<E> extends AbstractList<E> implements List<E> {
//在該類裡,E可以作為型別使用
public void set(E e) {
.......................
}
}
複製程式碼
2.5 泛型類派生子類
父類派生子類的時候不能在包含型別形參,需要傳入具體的型別
- 錯誤的方式:
public class A extends Container<K,V> {}
- 正確的方式:
public class A extends Container<Integer,String> {}
- 也可以不指定具體的型別,系統就會把K,V形參當成Object型別處理
public class A extends Container {}
2.6 泛型構造器
- 構造器也是一種方法,所以也就產生了所謂的泛型構造器。
- 和使用普通方法一樣沒有區別,一種是顯示指定泛型引數,另一種是隱式推斷
public class Person {
public <T> Person(T t) {
System.out.println(t);
}
}
複製程式碼
使用:
public static void main(String[] args) {
new Person(22);// 隱式
new <String> Person("hello");//顯示
}
複製程式碼
-
特殊說明:
- 如果構造器是泛型構造器,同時該類也是一個泛型類的情況下應該如何使用泛型構造器:因為泛型構造器可以顯式指定自己的型別引數(需要用到菱形,放在構造器之前),而泛型類自己的型別實參也需要指定(菱形放在構造器之後),這就同時出現了兩個菱形了,這就會有一些小問題,具體用法再這裡總結一下。 以下面這個例子為代表
public class Person<E> { public <T> Person(T t) { System.out.println(t); } } 複製程式碼
正確用法:
public static void main(String[] args) { Person<String> person = new Person("sss"); } 複製程式碼
PS:編譯器會提醒你怎麼做的
2.7 高階萬用字元
2.7.1背景:
2.7.2 <? extends T> 上界萬用字元
-
上界萬用字元顧名思義,<? extends T>表示的是型別的上界【包含自身】,因此通配的引數化型別可能是T或T的子類。
- 正因為無法確定具體的型別是什麼,add方法受限(可以新增null,因為null表示任何型別),但可以從列表中獲取元素後賦值給父型別。如上圖中的第一個例子,第三個add()操作會受限,原因在於List和List是List<? extends Animal>的子型別。
它表示集合中的所有元素都是Animal型別或者其子類 List<? extends Animal> 複製程式碼
-
這就是所謂的上限萬用字元,使用關鍵字extends來實現,例項化時,指定型別實參只能是extends後型別的子類或其本身。
- 例如:
- 這樣就確定集合中元素的型別,雖然不確定具體的型別,但最起碼知道其父類。然後進行其他操作。
//Cat是其子類 List<? extends Animal> list = new ArrayList<Cat>(); 複製程式碼
2.7.3 <? super T> 下界萬用字元
-
下界萬用字元<? super T>表示的是引數化型別是T的超型別(包含自身),層層至上,直至Object
- 編譯器無從判斷get()返回的物件的型別是什麼,因此get()方法受限。但是可以進行add()方法,add()方法可以新增T型別和T型別的子型別,如第二個例子中首先添加了一個Cat型別物件,然後添加了兩個Cat子型別別的物件,這種方法是可行的,但是如果新增一個Animal型別的物件,顯然將繼承的關係弄反了,是不可行的。
它表示集合中的所有元素都是Cat型別或者其父類 List <? super Cat> 複製程式碼
-
這就是所謂的下限萬用字元,使用關鍵字super來實現,例項化時,指定型別實參只能是extends後型別的子類或其本身
- 例如
//Animal是其父類 List<? super Cat> list = new ArrayList<Animal>(); 複製程式碼
2.7.4 <?> 無界萬用字元
- 任意型別,如果沒有明確,那麼就是Object以及任意的Java類了
- 無界萬用字元用<?>表示,?代表了任何的一種型別,能代表任何一種型別的只有null(Object本身也算是一種型別,但卻不能代表任何一種型別,所以List和List的含義是不同的,前者型別是Object,也就是繼承樹的最上層,而後者的型別完全是未知的)
3、泛型擦除
3.1 概念
編譯器編譯帶型別說明的集合時會去掉型別資訊
3.2 驗證例項:
public class GenericTest {
public static void main(String[] args) {
new GenericTest().testType();
}
public void testType(){
ArrayList<Integer> collection1 = new ArrayList<Integer>();
ArrayList<String> collection2= new ArrayList<String>();
System.out.println(collection1.getClass()==collection2.getClass());
//兩者class型別一樣,即位元組碼一致
System.out.println(collection2.getClass().getName());
//class均為java.util.ArrayList,並無實際型別引數資訊
}
}
複製程式碼
- 輸出結果:
true
java.util.ArrayList
複製程式碼
- 分析:
- 這是因為不管為泛型的型別形參傳入哪一種型別實參,對於Java來說,它們依然被當成同一類處理,在記憶體中也只佔用一塊記憶體空間。從Java泛型這一概念提出的目的來看,其只是作用於程式碼編譯階段,在編譯過程中,對於正確檢驗泛型結果後,會將泛型的相關資訊擦出,也就是說,成功編譯過後的class檔案中是不包含任何泛型資訊的。泛型資訊不會進入到執行時階段。
- 在靜態方法、靜態初始化塊或者靜態變數的宣告和初始化中不允許使用型別形參。由於系統中並不會真正生成泛型類,所以instanceof運運算元後不能使用泛型類
4、泛型與反射
- 把泛型變數當成方法的引數,利用Method類的getGenericParameterTypes方法來獲取泛型的實際型別引數
- 例子:
public class GenericTest {
public static void main(String[] args) throws Exception {
getParamType();
}
/*利用反射獲取方法引數的實際引數型別*/
public static void getParamType() throws NoSuchMethodException{
Method method = GenericTest.class.getMethod("applyMap",Map.class);
//獲取方法的泛型引數的型別
Type[] types = method.getGenericParameterTypes();
System.out.println(types[0]);
//引數化的型別
ParameterizedType pType = (ParameterizedType)types[0];
//原始型別
System.out.println(pType.getRawType());
//實際型別引數
System.out.println(pType.getActualTypeArguments()[0]);
System.out.println(pType.getActualTypeArguments()[1]);
}
/*供測試引數型別的方法*/
public static void applyMap(Map<Integer,String> map){
}
}
複製程式碼
- 輸出結果:
java.util.Map<java.lang.Integer,java.lang.String>
interface java.util.Map
class java.lang.Integer
class java.lang.String
複製程式碼
- 通過反射繞開編譯器對泛型的型別限制
public static void main(String[] args) throws Exception {
//定義一個包含int的連結串列
ArrayList<Integer> al = new ArrayList<Integer>();
al.add(1);
al.add(2);
//獲取連結串列的add方法,注意這裡是Object.class,如果寫int.class會丟擲NoSuchMethodException異常
Method m = al.getClass().getMethod("add",Object.class);
//呼叫反射中的add方法加入一個string型別的元素,因為add方法的實際引數是Object
m.invoke(al,"hello");
System.out.println(al.get(2));
}
複製程式碼
5 泛型的限制
5.1 模糊性錯誤
- 對於泛型類User<K,V>而言,宣告瞭兩個泛型類引數。在類中根據不同的型別引數過載show方法。
public class User<K,V> {
public void show(K k) { // 報錯資訊:'show(K)' clashes with 'show(V)'; both methods have same erasure
}
public void show(V t) {
}
}
複製程式碼
由於泛型擦除,二者本質上都是Obejct型別。方法是一樣的,所以編譯器會報錯。
換一個方式:
public class User<K,V> {
public void show(String k) {
}
public void show(V t) {
}
}
複製程式碼
使用結果:
可以正常的使用5.2 不能例項化型別引數
編譯器也不知道該建立那種型別的物件
public class User<K,V> {
private K key = new K(); // 報錯:Type parameter 'K' cannot be instantiated directly
}
複製程式碼
5.3 對靜態成員的限制
靜態方法無法訪問類上定義的泛型;如果靜態方法操作的型別不確定,必須要將泛型定義在方法上。
如果靜態方法要使用泛型的話,必須將靜態方法定義成泛型方法。
public class User<T> {
//錯誤
private static T t;
//錯誤
public static T getT() {
return t;
}
//正確
public static <K> void test(K k) {
}
}
複製程式碼
5.4 對泛型陣列的限制
- 不能例項化元素型別為型別引數的陣列,但是可以將陣列指向型別相容的陣列的引用
public class User<T> {
private T[] values;
public User(T[] values) {
//錯誤,不能例項化元素型別為型別引數的陣列
this.values = new T[5];
//正確,可以將values 指向型別相容的陣列的引用
this.values = values;
}
}
複製程式碼
5.5 對泛型異常的限制
泛型類不能擴充套件 Throwable,意味著不能建立泛型異常類 答案連結