一、什么是泛型?為什么要使用泛型?
泛型,即“參數化類型”。一提到參數,最熟悉的就是定義方法時有形參,然后調用此方法時傳遞實參。那么參數化類型怎么理解呢?顧名思義,就是將類型由原來的具體的類型參數化,類似于方法中的變量參數,此時類型也定義成參數形式(可以稱之為類型形參),然后在使用/調用時傳入具體的類型(類型實參)。
泛型的本質是為了參數化類型(在不創(chuàng)建新的類型的情況下,通過泛型指定的不同類型來控制形參具體限制的類型)。也就是說在泛型使用過程中,操作的數據類型被指定為一個參數,這種參數類型可以用在類、接口和方法中,分別被稱為泛型類、泛型接口、泛型方法。
沒有泛型之前:
private static void genericTest() {
List arrayList = new ArrayList();
arrayList.add("總有刁民想害朕");
arrayList.add(7);
for (int i = 0; i < arrayList.size(); i++) {
Object item = arrayList.get(i);
if (item instanceof String) {
String str = (String) item;
System.out.println("泛型測試 item = " + str);
}else if (item instanceof Integer)
{
Integer inte = (Integer) item;
System.out.println("泛型測試 item = " + inte);
}
}
}
如上代碼所示,在沒有泛型之前 類型的檢查 和 類型的強轉 都必須由我們程序員自己負責,一旦我們犯了錯,就是一個運行時崩潰等著我們。
有了泛型之后:
private static void genericTest2() {
List<String> arrayList = new ArrayList<>();
arrayList.add("總有刁民想害朕");
arrayList.add(7); //..(參數不匹配:int 無法轉換為String)
...
}
如上代碼,編譯器在編譯時期即可完成 類型檢查 工作,并提出錯誤(其實IDE在代碼編輯過程中已經報紅了)
二、泛型的特性是什么?
大家都知道,Java的泛型是偽泛型,這是因為Java在編譯期間,所有的泛型信息都會被擦掉,正確理解泛型概念的首要前提是理解類型擦除。Java的泛型基本上都是在編譯器這個層次上實現的,在生成的字節(jié)碼中是不包含泛型中的類型信息的,使用泛型的時候加上類型參數,在編譯器編譯的時候會去掉,這個過程成為類型擦除。
如在代碼中定義
List<Object>
和List<String>
等類型,在編譯后都會變成List
,JVM看到的只是List,而由泛型附加的類型信息對JVM是看不到的。Java編譯器會在編譯時盡可能的發(fā)現可能出錯的地方,但是仍然無法在運行時刻出現的類型轉換異常的情況,類型擦除也是Java的泛型與++模板機制實現方式之間的重要區(qū)別。
什么是類型擦除?
類型擦除指的是通過類型參數合并,將泛型類型實例關聯(lián)到同一份字節(jié)碼上。編譯器只為泛型類型生成一份字節(jié)碼,并將其實例關聯(lián)到這份字節(jié)碼上。類型擦除的關鍵在于從泛型類型中清除類型參數的相關信息,并且再必要的時候添加類型檢查和類型轉換的方法。 類型擦除可以簡單的理解為將泛型java代碼轉換為普通java代碼,只不過編譯器更直接點,將泛型java代碼直接轉換成普通java字節(jié)碼。 類型擦除的主要過程如下: 1.將所有的泛型參數用其最左邊界(最頂級的父類型)類型替換。 2.移除所有的類型參數。
通過兩個例子來理解泛型的類型擦除。
例一:
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass());
}
}
在這個例子中,我們定義了兩個ArrayList
數組,不過一個是ArrayList<String>
泛型類型的,只能存儲字符串;一個是ArrayList<Integer>
泛型類型的,只能存儲整數,最后,我們通過list1
對象和list2
對象的getClass()
方法獲取他們的類的信息,最后發(fā)現結果為true
。說明泛型類型String
和Integer
都被擦除掉了,只剩下原始類型。
例二:通過反射添加其它類型元素
public class Test {
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); //這樣調用 add 方法只能存儲整形,因為泛型類型的實例為 Integer
list.getClass().getMethod("add", Object.class).invoke(list, "asd");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
在程序中定義了一個ArrayList
泛型類型實例化為Integer
對象,如果直接調用add()
方法,那么只能存儲整數數據,不過當我們利用反射調用add()
方法的時候,卻可以存儲字符串,這說明了Integer
泛型實例在編譯之后被擦除掉了,只保留了原始類型。
類型擦除后保留的原始類型,那么什么是原始類型呢?
原始類型 就是擦除去了泛型信息,最后在字節(jié)碼中的類型變量的真正類型,無論何時定義一個泛型,相應的原始類型都會被自動提供,類型變量擦除,并使用其限定類型(無限定的變量用Object)替換。
例三:原始類型Object
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Pair的原始類型為:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
因為在Pair<T>
中,T 是一個無限定的類型變量,所以用Object
替換,其結果就是一個普通的類,如同泛型加入Java語言之前的已經實現的樣子。在程序中可以包含不同類型的Pair
,如Pair<String>
或Pair<Integer>
,但是擦除類型后他們的就成為原始的Pair
類型了,原始類型都是Object
。
從上面的例2中,我們也可以明白ArrayList<Integer>
被擦除類型后,原始類型也變?yōu)?code>Object,所以通過反射我們就可以存儲字符串了。
如果類型變量有限定,那么原始類型就用第一個邊界的類型變量類替換。
比如: Pair這樣聲明的話:
public class Pair<T extends Comparable> {}
那么原始類型就是Comparable
。
要區(qū)分原始類型和泛型變量的類型。
在調用泛型方法時,可以指定泛型,也可以不指定泛型。
- 在不指定泛型的情況下,泛型變量的類型為該方法中的幾種類型的同一父類的最小級,直到Object
- 在指定泛型的情況下,該方法的幾種類型必須是該泛型的實例的類型或者其子類
public class Test {
public static void main(String[] args) {
/**不指定泛型的時候*/
int i = Test.add(1, 2); //這兩個參數都是Integer,所以T為Integer類型
Number f = Test.add(1, 1.2); //這兩個參數一個是Integer,以風格是Float,所以取同一父類的最小級,為Number
Object o = Test.add(1, "asd"); //這兩個參數一個是Integer,以風格是Float,所以取同一父類的最小級,為Object
/**指定泛型的時候*/
int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能為Integer類型或者其子類
int b = Test.<Integer>add(1, 2.2); //編譯錯誤,指定了Integer,不能為Float
Number c = Test.<Number>add(1, 2.2); //指定為Number,所以可以為Integer和Float
}
//這是一個簡單的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
其實在泛型類中,不指定泛型的時候,也差不多,只不過這個時候的泛型為Object
,就比如ArrayList
中,如果不指定泛型,那么這個ArrayList
可以存儲任意的對象。
三、泛型的使用方式
泛型一般有三種使用方式:泛型類、泛型接口、泛型方法。
1.泛型類就是把泛型定義在類上,用戶使用該類的時候,才把類型明確下來
這樣的話,用戶明確了什么類型,該類就代表著什么類型…用戶在使用的時候就不用擔心強轉的問題,運行時轉換異常的問題了。
/**
* Java泛型
*/
public class Demo {
public static void main(String[] args) {
// 定義泛型類 Test 的一個Integer版本
Test<Integer> intOb = new Test<Integer>(88);
intOb.showType();
int i = intOb.getOb();
System.out.println("value= " + i);
System.out.println("----------------------------------");
// 定義泛型類Test的一個String版本
Test<String> strOb = new Test<String>("Hello Gen!");
strOb.showType();
String s = strOb.getOb();
System.out.println("value= " + s);
}
}
/*
使用T代表類型,無論何時都沒有比這更具體的類型來區(qū)分它。如果有多個類型參數,我們可能使用字母表中T的臨近的字母,比如S。
*/
class Test<T> {
private T ob;
/*
定義泛型成員變量,定義完類型參數后,可以在定義位置之后的方法的任意地方使用類型參數,就像使用普通的類型一樣。
注意,父類定義的類型參數不能被子類繼承。
*/
//構造函數
public Test(T ob) {
this.ob = ob;
}
//getter 方法
public T getOb() {
return ob;
}
//setter 方法
public void setOb(T ob) {
this.ob = ob;
}
public void showType() {
System.out.println("T的實際類型是: " + ob.getClass().getName());
}
}
/* output
T的實際類型是: java.lang.Integer
value= 88
----------------------------------
T的實際類型是: java.lang.String
value= Hello Gen!
*/
2.泛型接口
public interface Generator<T> {
public T method();
}
實現泛型接口,不指定類型:
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
實現泛型接口,指定類型:
class GeneratorImpl<T> implements Generator<String>{
@Override
public String method() {
return "hello";
}
}
泛型方法:判斷一個方法是否是泛型方法關鍵看方法返回值前面有沒有使用 <>
標記的類型,有就是,沒有就不是
public class Normal {
// 成員泛型方法
public <E> String getString(E e) {
return e.toString();
}
// 靜態(tài)泛型方法
public static <V> void printString(V v) {
System.out.println(v.toString());
}
}
// 泛型類中的泛型方法
public class Generics<T> {
// 成員泛型方法
public <E> String getString(E e) {
return e.toString();
}
// 靜態(tài)泛型方法
public static <V> void printString(V v) {
System.out.println(v.toString());
}
}
四、Java中的泛型通配符
常用的 T,E,K,V,?
本質上這些個都是通配符,沒啥區(qū)別,只不過是編碼時的一種約定俗成的東西。比如上述代碼中的 T ,我們可以換成 A-Z 之間的任何一個 字母都可以,并不會影響程序的正常運行,但是如果換成其他的字母代替 T ,在可讀性上可能會弱一些。通常情況下,T,E,K,V,? 是這樣約定的:
- ? 表示不確定的 java 類型
- T (type) 表示具體的一個java類型
- K V (key value) 分別代表java鍵值中的Key Value
- E (element) 代表Element
? 無界通配符:
我有一個父類 Animal 和幾個子類,如狗、貓等,現在我需要一個動物的列表,我的第一個想法是像這樣的:
List<Animal> listAnimals
但是老板的想法確實這樣的:
List<? extends Animal> listAnimals
為什么要使用通配符而不是簡單的泛型呢?通配符其實在聲明局部變量時是沒有什么意義的,但是當你為一個方法聲明一個參數時,它是非常重要的。
static int countLegs (List<? extends Animal > animals ) {
int retVal = 0;
for ( Animal animal : animals )
{
retVal += animal.countLegs();
}
return retVal;
}
static int countLegs1 (List< Animal > animals ){
int retVal = 0;
for ( Animal animal : animals )
{
retVal += animal.countLegs();
}
return retVal;
}
public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
// 不會報錯
countLegs( dogs );
// 報錯
countLegs1(dogs);
}
當調用 countLegs1 時,就會飄紅,提示的錯誤信息如下:
所以,對于不確定或者不關心實際要操作的類型,可以使用無限制通配符(尖括號里一個問號,即 <?> ),表示可以持有任何類型。像 countLegs 方法中,限定了上屆,但是不關心具體類型是什么,所以對于傳入的 Animal 的所有子類都可以支持,并且不會報錯。而 countLegs1 就不行。
上界通配符 < ? extends E>:
上屆:用 extends 關鍵字聲明,表示參數化的類型可能是所指定的類型,或者是此類型的子類。
在類型參數中使用 extends 表示這個泛型中的參數必須是 E 或者 E 的子類,這樣有兩個好處:
如果傳入的類型不是 E 或者 E 的子類,編譯不成功泛型中可以使用 E 的方法,要不然還得強轉成 E 才能使用
下界通配符 < ? super E>:
下界: 用 super 進行聲明,表示參數化的類型可能是所指定的類型,或者是此類型的父類型,直至 Object
到此本篇關于Java基礎之泛型的詳細知識點總結的文章就介紹到這了,想要了解更多相關Java泛型的詳細內容,請搜索W3Cschool以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持!