Java泛型總結


0. 概述

泛型是Jdk1.5引入的特性。泛型是Java程序員最常用且最容易被忽視的知識之一。許多Java程序員只是使用泛型類。但不考慮其工作方式,直到出現問題。

 

 

 

1 術語

用示例進行描述。ArrayList<E>類、ArrayList<Integer>類:

  • 整個稱為ArrayList<E> 泛型類型
  • ArrayList<E>中的E稱為 類型變量 類型參數
  • 整個ArrayList<Integer> 稱為 參數化的類型
  • ArrayList<Integer>中的Integer稱為 類型參數的實例 或 實際類型參數
  • ArrayList<Integer>中的<>念着typeof。整個ArrayList<Integer>稱為"arraylist typof integer"。
  • ArrayList稱為原始類型。

 

 

 

2 泛型使用示例

由於經常需要使用泛型。所以不再詳解泛型的實際使用。

此處示例一種遍歷Map的方式。

        Map<String, Integer> hm = new HashMap<String, Integer>();

        hm.put("zxx", 19);

        hm.put("lis", 18);

 

        Set<Map.Entry<String, Integer>> mes = hm.entrySet();

        for (Map.Entry<String, Integer> me : mes) {

            System.out.println(me.getKey() + ":" + me.getValue());

        }

 

附:對在jsp頁面中也經常要對Set或Map集合進行迭代。

<c:forEach items="${map}" var="entry">

    ${entry.key}:${entry.value}

</c:forEach>

 

 

 

3 不使用泛型產生的困難

從集合中取出數據需要類型轉換。從集合中取出的數據需要造型,但由於集合中元素的類型沒有被限制,所以可能造成類型轉換錯誤。

使用泛型可以不需要再使用強制類型轉換。且可以確保類型安全。

由於編譯生成的字節碼會去掉泛型的類型信息,只要能跳過編譯器,就可以往某個泛型集合中加入其它類型的數據。例,用反射得到集合,再調用其add方法即可。

 

 

 

4 Java泛型原理

Java中的泛型類型(或者泛型)類從表面上似於 C++ 中的模板,但兩者機制有本質的不同。

Java的泛型方法沒有C++模板函數功能強大,Java中的如下代碼無法通過編譯:

<T> T add(T x,T y) {

    return (T) (x+y);

}

Java中無論何時定義一個泛型類型,都自動提供一個相應的原始類型。原始類型的名字就是刪去類型參數后的泛型類型名。

Java 中的泛型基本上是在編譯器中實現。編譯器進行執行類型檢查和類型推斷,然后生成普通的非泛型的字節碼。編譯器使用泛型類型信息保證類型安全,然后在生成字節碼之前將其清除。這種實現技術稱為擦除(erasure)。

 所以泛型實際是提供給Javac編譯器使用的。限定輸入類型,讓編譯器擋住源程序中的非法輸入,編譯器編譯帶類型說明的集合時會去除掉"類型"信息。程序運行期間,沒有任何泛型泛型的痕跡。使程序運行效率不受影響,對於參數化的泛型類型,getClass()方法的返回值和原始類型完全一樣。

例:通過以下代碼,可以看出運行期泛型類型是被擦除的。

import java.util.ArrayList;

import java.util.List;

 

public class TestMain {

    public static void main(String[] args) {

 

        List l = new ArrayList();

        System.out.println(l.getClass());

        

        List<String> ls = new ArrayList<String>();

        System.out.println(ls.getClass());

        

        List<Object> lo = new ArrayList<Object>();

        System.out.println(lo.getClass());

        

        System.out.println(l.getClass().equals(ls.getClass())); //比較ArrayList與ArrayList<String>

        System.out.println(l.getClass().equals(lo.getClass())); //比較ArrayList與List<Object>

        System.out.println(lo.getClass().equals(ls.getClass())); //比較ArrayList<String>與List<Object>

        

    }

}

輸出:

class java.util.ArrayList

class java.util.ArrayList

class java.util.ArrayList

true

true

true

 

 

例:使用反射機制繞過泛型的編譯器檢查。由於程序在運行期間,不再帶有泛型約束,所以程序正確打印結果。

public class TestMain {

    public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {

        List<Integer> ls = new ArrayList<Integer>();

        //ls.add("abc"); //編譯器報錯

        ls.getClass().getMethod("add", Object.class).invoke(ls,"abc");

        System.out.println(ls);

    }

}

輸出:

[abc]

 

 

 

5 正確使用泛型

5.1 參數化類型與原始類型的兼容性

參數化類型可以引用一個原始類型的對象。編譯器提示警告。

例,Collection<String> c = new ArrayList();

 

原始類型可以引用一個參數化類型的對象。編譯器提示警告。

例,Collection c = new ArrayList<String>();

 

參數化類型不考慮類型參數的繼承關系。

例:ArrayList<String> a = new ArrayList<Object>(); 編譯器提示錯誤。

例:ArrayList<Object> a = new ArrayList<String>(); 編譯器提示錯誤。

 

編譯器不允許創建泛型變量的數組。即在創建數組實例時,數組的元素不能使用參數化的類型

例:ArrayList<Integer>[] a1 = new ArrayList<Integer>[10];

或 ArrayList<Integer> a2[] = new ArrayList<Integer>[10]; 編譯器提示錯誤。

 

強調,泛型只是在編譯期間編譯器做類型檢查的機制。

例:以下示例可以通過編譯。編譯器只是提示警告。

此處的a對象,本質上變為ArrayList<Object> 。

此處的a對象,本質上變為ArrayList<String> 。

 

 

5.2 Java泛型的約束和局限性

(1)不能使用基本類型實例化類型參數

不存在Pair<double>,只有Pair<Double>。由於類型擦除,Pair類含有Object類型的域,而Object不能存儲double。

但該缺陷,可以使用包裝類型解決。

重要結論:使用泛型或定義泛型類、泛型方法時,泛型必須是引用類型,不能是基本類型。

 

(2)不能拋出也不能捕獲泛型類實例

泛型類不能繼承Throwable。

不能在catch子句中使用泛型類型變量。

 

(3)參數化類型的數組不合法

 

(4)不能實例化類型變量

若需要在方法中對泛型類型實例化,則需要傳入Class<T>

public static <T> void makePair(Class<T> cl) throws InstantiationException, IllegalAccessException{

T t = cl.newInstance();

}

 

(5)泛型類的靜態上下文中類型變量無效

不能在靜態域或方法中應用類型變量。

 

(6)擦除后的沖突

以下代碼無法通過編譯

equals(T obj)equals(Object obj)沖突。擦除類型后,equals(T obj)就是equals(Object obj)

 

 

 

6 定義泛型類

泛型類是具有一個或多個類型變量的類。可以把泛型類看作是普通類的"工廠"。

自定義泛型使用尖括號<>聲明泛型參數。泛型類基本格式:

class CLS<T, U> {

    void method1(T param) {

    }

 

    T method2(T param) {

        // return

    }

 

    U method3(T param1, U param2) {

        // return ;

    }

}

 

一般Java中,使用變量E表示集合元素類型。K表示關鍵字,V表示值。T(或者U、S)表示"任意類型"。

當一個變量被聲明為泛型時,只能被實例變量、方法和內部類調用,而不能被靜態變量和靜態方法調用。因為靜態成員是被所有參數化的類所共享的,所以靜態成員不應該有類級別的類型參數。若使用需要是靜態泛型方法,則重新聲明泛型參數。

 

例:定義泛型GenericDao 。

public class GenericDao<T> {

    public void save(T obj) {

    }

    public T getById(int id) {

        return null;

    }

    /**

     * 靜態方法。重新聲明T,此處的T與GenericDao<T>的T沒有關系。

     */

    public static <T> void update(T obj){

    }

}

 

GenericDao引入類型變量T,用尖括號<> 括起來,並放在類名后面。泛型類可以有多個類型變量,如:GenericDao<T,U>

 

在引用類名時指定的泛型類的泛型參數類型。

例,如下兩種方式都可以:

GenericDao<String> dao = null;

    new GenericDao<String>();

 

 

7 自定義泛型方法

用於放置泛型的類型參數的尖括號應出現在方法的其他所有修飾符之后和在方法的返回類型之前,也就是緊鄰返回值之前。

按照慣例,類型參數通常用單個大寫字母表示。

普通方法、構造方法和靜態方法中都可以使用泛型。

 

例:

原方法為:

    static int method(int x,int y){

        return 0;

    }

    

泛型方法為:

    static <T> T method(T x,T y){

        return null;

    }

<T>用於聲明一個泛型參數。

 

7.1 使用泛型化方法

當調用一個泛型方法時,在方法名前的尖括號中放入具體的類型。

例:使用調用TestMain類中的泛型化的靜態方法method。

String s = TestMain.<String>method("a","b");

必須有類名在最前面引出method方法,否則編譯器報錯。

 

由於編譯器存在類型推斷機制,會自動推斷出調用方法的泛型類型。所以使用泛型方法可以不需要指明泛型類型。

例:調用泛型化的method()方法。

 

例:交換數組中的兩個元素的位置的泛型方法語法定義如下:

    static <T> void swap(T[] a,int i,int j){

        T tmp = a[i];

        a[j]=a[i];

        a[i]=tmp;

    }

使用swap方法:

 

說明:

只有引用類型才能作為泛型方法的實際參數。swap(new int[]{1,2,3,4},1,3);編譯錯誤。

 

在泛型中可以同時有多個類型參數,在定義它們的尖括號中用逗號分。

例:

public static <K, V> V getValue(Map<K,V> map,K key) {

        return map.get(key);

}

 

 

7.2 泛型方法例題

例:編寫一個泛型方法,自動將Object類型的對象轉換成其他類型。

解:

public static <T> T autoConvert(Object obj){

        return (T)obj;

    }

使用autoConvert方法。

    Object obj = "abc";

    String abc1 = (String)obj;

    String abc2 = autoConvert(obj);

 

例:定義一個方法,可以將任意類型的數組中的所有元素填充為相應類型的某個對象。

    static <T> void fillArray(T[] a,T obj){

        for(int i=0;i<a.length;i++){

            a[i]=obj;

        }

    }

 

例:采用自定泛型方法的方式打印出任意參數化類型的集合中的所有內容。

    static <T> void printCollection(Collection<T> collection){

        for (Object obj : collection) {

            System.out.println(obj);

        }

    }

說明:該例使用之前的通配符方案要比范型方法更有效。

當一個類型變量用來表達兩個參數之間或者參數和返回值之間的關系時,即同一個類型變量在方法簽名的兩處被使用,或者類型變量在方法體代碼中也被使用而不是僅在簽名的時候使用,才需要使用范型方法。

 

 

例:定義一個方法,把任意參數類型的集合中的數據安全地復制到相應類型的數組中。

static <T> void copy1(Collection<T> dest,T[] src){

        // ...略

    }

    

    static <T> void copy2(T[] dest,T[] src){

        // ...略

    }

 

使用copy1方法和copy2方法:

//copy1泛型方法的T的類型Object。因為ArrayList與String父類的交集為Object。

copy1(new ArrayList(),new String[10]);

//copy2泛型方法的T的類型Object。因為Date與String父類的交集為Object。

copy2(new Date[10],new String[10]);

 

//new ArrayList<Date>()確定了T為Data,而new String[10]又指明T為String。

//由於類型推斷的傳播性,導致編譯器報錯。

copy1(new ArrayList<Date>(),new String[10]);

 

 

8 泛型類型變量的限定

使用< T extends BoundingType >限定泛型類型。T應該是綁定類型的子類型。T和綁定類型可以是類或接口。

例:<T extends Comparable & Serializable>

 

例:Class.getAnnotation()方法的定義。

public <A extends Annotation> A getAnnotation(Class<A> annotationClass)

<A extends Annotation> 表示聲明一個泛型參數,該參數類型必須是Annotation類或其的子類。

 

例:通用泛型Dao。

public interface GenericDao<T extends Serializable,ID extends Serializable> {

}

 

public abstract class GenericDaoImpl<T extends Serializable ,ID extends Serializable> implements GenericDao<T, ID> {

}

 

一個泛型變量或通配符可以有多個限定。限定類型使用"&"分割。若用一個類做限定,它必須是限定列表中的第一個。

例:<V extends Serializable & Cloneable> void method(){}

<V extends Serializable & cloneable> 表示聲明一個泛型參數,參數類型必須繼承Serializable 且必須繼承Cloneable

 

可以用類型變量表示異常,稱為參數化的異常,可以用於方法的throws列表中,但是不能用於catch子句中。

例:

private static <T extends Exception> void sayHello() throws T {

try {

 

        } catch (Exception e) {

            throw (T) e;

    }

}

 

 

 

9 泛型通配符

使用"?"通配符可以引用其他各種參數化的類型,"?"通配符定義的變量主要用作引用,可以調用與參數化無關的方法,不能調用與參數化有關的方法。

例:Collection<?>可以適配Collection<Object>Collection<Integer>Collection<String>等。

 

例:定義一個方法,該方法用於打印出任意參數化類型的集合中的所有數據,該方法如何定義呢?

錯誤方式:

    public static void printCollection(Collection<Object> cols) {

        for (Object obj : cols) {

            System.out.println(obj);

        }

    }

說明:該方法只能接收Collection<Object>類型的參數。對Collection<Integer>Collection<String>無法接收。

正確方式:

不使用泛型,編譯器提示警告。

    public static void printCollection(Collection cols) {

        for (Object obj : cols) {

            System.out.println(obj);

        }

    }

利用泛型通配符,使用泛型。

    public static void printCollection(Collection<?> cols) {

        for (Object obj : cols) {

            System.out.println(obj);

        }

    }

說明:該方法可以接收Collection<Integer>Collection<Object>Collection<String>等。

 

參數通配符使用的聲明的類型,是不確定類型的。在使用的時候,可能會造成一些錯誤。

例:定義兩個方法method1與method2。

 

 

9.1 限定通配符的上邊界

<? extends Number>用於匹配Number及Number的子類。

正確:Vector<? extends Number> x = new Vector<Integer>(); Integer繼承了Number

錯誤:Vector<? extends Number> x = new Vector<String>();

 

9.2限定通配符的下邊界

<? super Integer>用於匹配Integer及Integer的父類。

正確:Vector<? super Integer> x = new Vector<Number>();

錯誤:Vector<? super Integer> x = new Vector<Byte>();

 

說明:

限定通配符總是包括自己。

?只能用作引用,不能用它去給其他變量賦值

    Vector<? extends Number> y = new Vector<Integer>();

    Vector<Number> x = y; 錯誤,原理與Vector<Object > x11 = new Vector<String>();相似,只能通過強制類型轉換方式來賦值。

 

 

10 泛型類型推斷

例,對於上面定義的static <T> T method(T x,T y)方法。注意其返回類型。

Integer i = method(1,2); //1與2都是Integer,所以返回Integer

Double d = method(1.2,2.3); //1.2,2.3默認都是Double,所以返回Double

Number n = method(1.2,2); //1.2是Double,2是Integer,兩者均能匹配Number。返回Number。

Object o = method(1.2,"2"); //1.2是Double,"2"是String,這者均能匹配Object。返回Object。

method的返回類型是參數x與參數y允許允許匹配類型的交集。

 

編譯器判斷范型方法的實際類型參數的過程稱為類型推斷。類型推斷是相對於知覺推斷的,其實現方法是一種非常復雜的過程。

根據調用泛型方法時實際傳遞的參數類型或返回值的類型來推斷,具體規則如下:

  • 當某個類型變量只在整個參數列表中的所有參數和返回值中的一處被應用了,那么根據調用方法時該處的實際應用類型來確定,這很容易憑着感覺推斷出來,即直接根據調用方法時傳遞的參數類型或返回值來決定泛型參數的類型。
    例: swap(new String[3],3,4) ---> static <E> void swap(E[] a, int i, int j)
  • 當某個類型變量在整個參數列表中的所有參數和返回值中的多處被應用了,如果調用方法時這多處的實際應用類型都對應同一種類型來確定,這很容易憑着感覺推斷出來。
    例:add(3,5) ---> static <T> T add(T a, T b)
  • 當某個類型變量在整個參數列表中的所有參數和返回值中的多處被應用了,如果調用方法時這多處的實際應用類型對應到了不同的類型,且沒有使用返回值,這時候取多個參數中的最大交集類型
    例:下面語句實際對應的類型就是Number了,編譯沒問題,只是運行時出問題。
    fill(new Integer[3],3.5f) ---> static <T> void fill(T[] a, T v)
  • 當某個類型變量在整個參數列表中的所有參數和返回值中的多處被應用了,如果調用方法時這多處的實際應用類型對應到了不同的類型, 並且使用返回值,這時候優先考慮返回值的類型。
    例如,下面語句實際對應的類型就是Integer了,編譯將報告錯誤,將變量x的類型改為float,對比eclipse報告的錯誤提示,接着再將變量x類型改為Number,則沒有了錯誤。
    int x =(3,3.5f) ---> static <T> T add(T a, T b)
  • 參數類型的類型推斷具有傳遞性,下面第一種情況推斷實際參數類型為Object,編譯沒有問題,而第二種情況則根據參數化的Vector類實例將類型變量直接確定為String類型,編譯將出現問題。
    例:copy(new Integer[5],new String[5]) ---> static <T> void copy(T[] a,T[] b);
    copy(new Vector<String>(), new Integer[5]) ---> static <T> void copy(Collection<T> a , T[] b);

 

 

 

11 通過反射獲取泛型類型

如List<Date> list= new ArrayList<Date>(); 通過反射獲取list的類型。這是很多框架內部實現的常見需求。

雖然Java編譯器會把泛型類型擦除,但擦除的類仍然保留一些泛型祖先的微弱記憶。

 

方法一:反射相關的Method對象,可以獲取其代表方法的參數所帶的泛型信息。

示例代碼:

public class GenericalReflection {

    private List<Date> dates = new ArrayList<Date>();

 

    public void setDates(List<Date> dates) {

        this.dates = dates;

    }

 

    public static void main(String[] args) throws SecurityException, NoSuchMethodException {

        //獲取setDates的Method對象

        Method methodApply = GenericalReflection.class.getDeclaredMethod("setDates", List.class);

        Type[] gTypes= methodApply.getGenericParameterTypes();

        ParameterizedType pType = (ParameterizedType) (gTypes)[0];

        Type[] actualTypeArguments = pType.getActualTypeArguments();

        //輸出Method對象泛型擦除后的類型。

        System.out.println(((Class)pType.getRawType()).getName());

        //輸出Method對象泛型信息。

System.out.println(((Class)actualTypeArguments[0]).getName());

    }

}

 

方法二:通過使用class實例的getGenericSuperclass()方法獲取泛型信息。

例:泛型DAO實現類中獲取泛型類型。

public interface BaseDao<T> {

}

 

@SuppressWarnings("unchecked")

public abstract class BaseDaoImpl<T> implements BaseDao<T> {

    protected Class<T> clazz;

 

    public BaseDaoImpl() {

        Type type = this.getClass().getGenericSuperclass();

        ParameterizedType pt = (ParameterizedType) type;

        this.clazz = (Class) pt.getActualTypeArguments()[0];

        System.out.println("clazz = " + this.clazz);

    }

}

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM