java 泛型 深入 – JAVA編程語言程序開發技術文章

泛型的好處:

    泛型的主要好處就是讓編譯器保留參數的類型信息,執行類型檢查,執行類型轉換(casting)操作,編譯器保證瞭這些類型轉換(casting)的絕對無誤。

        /******* 不使用泛型類型 *******/
        List list1 = new ArrayList();
        list1.add(8080);                                  //編譯器不檢查值
        String str1 = (String)list1.get(0); //需手動強制轉換,如轉換類型與原數據類型不一致將拋出ClassCastException異常
       
        /******* 使用泛型類型 *******/
        List<String> list2 = new ArrayList<String>();
        list2.add("value");                 //[類型安全的寫入數據] 編譯器檢查該值,該值必須是String類型才能通過編譯
        String str2 = list2.get(0); //[類型安全的讀取數據] 不需要手動轉換
       

泛型的類型擦除:

    Java 中的泛型隻存在於編譯期,在將 Java 源文件編譯完成 Java 字節代碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會被編譯器在編譯的時候去掉。

    這個過程就稱為類型擦除(type erasure)。

        List<String>    list1 = new ArrayList<String>();
        List<Integer> list2 = new ArrayList<Integer>();
       
        System.out.println(list1.getClass() == list2.getClass()); // 輸出結果: true
        System.out.println(list1.getClass().getName()); // 輸出結果: java.util.ArrayList
        System.out.println(list2.getClass().getName()); // 輸出結果: java.util.ArrayList
       

在以上代碼中定義的 List<String> 和 List<Integer> 等類型,在編譯之後都會變成 List,而由泛型附加的類型信息對 JVM 來說是不可見的,所以第一條打印語句輸出 true,

第二、第三條打印語句都輸出 java.util.ArrayList,這都說明 List<String> 和 List<Integer> 的對象使用的都是同一份字節碼,運行期間並不存在泛型。

來看一個簡單的例子:

package test;

import java.util.List;
/**
 * —————————————–
 * @描述  類型擦除
 * @作者  fancy
 * @郵箱  fancydeepin@yeah.net
 * @日期  2012-8-25 <p>
 * —————————————–
 */
public class GenericsApp {

   
    public void method(List<String> list){
       
    }
   
    /*
     * 編譯出錯,這兩個方法不屬於重載,由於類型的擦除,使得這兩個方法的參數列表的參數均為List類型,
     * 這就相當於同一個方法被聲明瞭兩次,編譯自然無法通過瞭
     *
    public void method(List<Integer> list){
       
    }
    */
   
}

以此類為例,在 cmd 中 編譯 GenericsApp.java 得到字節碼(泛型已經擦除),然後再反編譯這份字節碼來看下源碼中泛型是不是真的被擦除瞭:

 

從圖中可以看出,經反編譯後的源碼中 method 方法的參數變成瞭 List 類型,說明泛型的類型是真的被擦除瞭,字節碼文件中不存在泛型,也就是說,運行期間泛型並不存在,它在

編譯完成之後就已經被擦除瞭。

泛型類型的子類型:

    泛型類型跟其是否是泛型類型的子類型沒有任何關系。

        List<Object> list1;
        List<String> list2;
       
        list1 = list2; // 編譯出錯
        list2 = list1; // 編譯出錯

大傢都知道,在 Java 中,Object 類是所有類的超類,自然而然的 Object 類是 String 類的超類,按理,將一個 String 類型的對象賦值給一個 Object 類型的對象是可行的,

但是泛型中並不存在這樣的邏輯,用更通俗的話說,泛型類型跟其是否子類型沒有任何關系。

泛型中的通配符(?):

    由於泛型類型與其子類型存在不相關性,那麼在不能確定泛型類型的時候,可以使用通配符(?),通配符(?)能匹配任意類型。

        List<?> list;
        List<Object> list1 = null;
        List<String>  list2 = null;
       
        list = list1;
        list = list2;

限定通配符的上界:

        ArrayList<? extends Number> collection = null;
       
        collection = new ArrayList<Number>();
        collection = new ArrayList<Short>();
        collection = new ArrayList<Integer>();
        collection = new ArrayList<Long>();
        collection = new ArrayList<Float>();
        collection = new ArrayList<Double>();
       

 ? extends XX,XX 類是用來限定通配符的上界,XX 類是能匹配的最頂層的類,它隻能匹配 XX 類以及 XX 類的子類。在以上代碼中,Number 類的實現類有:

AtomicInteger、AtomicLong、 BigDecimal、 BigInteger、 Byte、 Double、 Float、 Integer、 Long、 Short ,因此以上代碼均無錯誤。

限定通配符的下界:

        ArrayList<? super Integer> collection = null;
       
        collection = new ArrayList<Object>();
        collection = new ArrayList<Number>();
        collection = new ArrayList<Integer>();
       

 ? super XX,XX 類是用來限定通配符的下界,XX 類是能匹配的最底層的類,它隻能匹配 XX 類以及 XX 類的超類,在以上代碼中,Integer 類的超類有:

Number、Object,因此以上代碼均能通過編譯無誤。

通過反射獲得泛型的實際類型參數:

    這個就有點難度瞭,上面已經說到,泛型的類型參數會在編譯完成以後被擦除,那在運行期間還怎麼來獲得泛型的實際類型參數呢?這個是有點難度瞭吧?似乎不可能實現的樣子,

    其實不然,java.lang.Class 類從 Java 1.5 起(如果沒記錯的話),提供瞭一個 getGenericSuperclass() 方法來獲取直接超類的泛型類型,這就使得獲取泛型的實際類型參數成為

    瞭可能,下面來看一段代碼,這段代碼很精辟,很有用,大傢一定要學到手哈:

package test;

import java.lang.reflect.ParameterizedType;
/**
 * —————————————–
 * @描述  泛型的實際類型參數
 * @作者  fancy
 * @郵箱  fancydeepin@yeah.net
 * @日期  2012-8-25 <p>
 * —————————————–
 */
public class Base<T> {

    private Class<T> entityClass;
   
    //代碼塊,也可將其放置到構造子中
    {
        entityClass =(Class<T>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];
           
    }
   
    //泛型的實際類型參數的類全名
    public String getEntityName(){
       
        return entityClass.getName();
    }
   
    //泛型的實際類型參數的類名
    public String getEntitySimpleName(){
       
        return entityClass.getSimpleName();
    }

    //泛型的實際類型參數的Class
    public Class<T> getEntityClass() {
        return entityClass;
    }
   
}

以上代碼的精華全在這句:(Class<T>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];

實際上,這句話咋看起來很難看的明白,理解起來就更加的吃力瞭,下面容我來將這句復雜的代碼拆分開來,理解起來可能會好些:

    //代碼塊,也可將其放置到構造子中
    {
        //entityClass =(Class<T>)((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        try {
            Class<?> clazz = getClass(); //獲取實際運行的類的 Class
            Type type = clazz.getGenericSuperclass(); //獲取實際運行的類的直接超類的泛型類型
            if(type instanceof ParameterizedType){ //如果該泛型類型是參數化類型
                Type[] parameterizedType = ((ParameterizedType)type).getActualTypeArguments();//獲取泛型類型的實際類型參數集
                entityClass = (Class<T>) parameterizedType[0]; //取出第一個(下標為0)參數的值
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
           
    }
   

註意,獲取 Class 實例的時候是用 getClass(),而不是用 Base.class,獲取 Class 的方式有三種,這是其中的兩種,還有一種是 Class.forName("類全名"),如需瞭解反射的基礎知識

請前往上一篇隨筆 java 反射基礎

那麼,Base.class 與 getClass(),這兩個方法來獲取類的字節碼的時候,有什麼不一樣的地方呢?當然有不一樣的地方瞭,Base.class 是寫死瞭的,它得到的永遠是 Base 類的字節碼,

而 getClass() 方法則不同,在上面代碼註釋中的第一、二行註釋我用瞭“實際運行的類”6個字,這幾個字很重要,一定要理解,如果無法理解,下面的你可能就看不懂瞭。

為瞭方便大傢的理解,下面插加一個小例子來加以說明 類.class 與 getClass() 兩種方法來獲取類的字節碼有什麼區別:

package test;
/**
 * —————————————–
 * @描述  超類
 * @作者  fancy
 * @郵箱  fancydeepin@yeah.net
 * @日期  2012-8-25 <p>
 * —————————————–
 */
public class Father {

    public Father (){
       
        System.out.println("Father 類的構造子:");
        System.out.println("Father.class :" + Father.class);
        System.out.println("getClass()      :" + getClass());
    }
   
}

package test;

/**
 * —————————————–
 * @描述  超類的子類(超類的實現類)
 * @作者  fancy
 * @郵箱  fancydeepin@yeah.net
 * @日期  2012-8-25 <p>
 * —————————————–
 */
public class Children extends Father{

   
}

package test;
/**
 * —————————————–
 * @描述  測試類
 * @作者  fancy
 * @郵箱  fancydeepin@yeah.net
 * @日期  2012-8-25 <p>
 * —————————————–
 */
public class Test {

    public static void main(String[] args){
       
        new Children(); //實際運行的類是Children(Father類的子類或者說是實現類)
    }
   
}

後臺打印輸出的結果:

Father 類的構造子:
Father.class :class test.Father
getClass()      :class test.Children

 從打印出的結果看來,類.class 與 getClass() 的區別很明瞭瞭,getClass() 獲取的是實際運行的類的字節碼,它不一定是當前類的 Class,有可能是當前類的子類的 Class,具體是哪

個類的 Class,需要根據實際運行的類來確定,new 哪個類,getClass() 獲取的就是哪個類的 Class,而 類.class 獲取得到的 Class 永遠隻能是該類的 Class,這點是有很大的區別的。

這下“實際運行的類”能理解瞭吧,那麼上面的那段被拆分的代碼也就不難理解瞭,getClass() 理解瞭那 clazz.getGenericSuperclass() 也就沒什麼問題瞭吧,千萬不要以為

clazz.getGenericSuperclass() 獲取得到的是 Object 類那就成瞭,實際上假如當前運行的類是 Base 類的子類,那麼 clazz.getGenericSuperclass() 獲取得到的就是 Base 類。

再者就是最後一句,(Class<T>) parameterizedType[0],怎麼就知道第一個參數(parameterizedType[0])就是該泛型的實際類型呢?很簡單,因為 Base<T> 的泛型的類型

參數列表中隻有一個參數,所以,第一個元素就是泛型 T 的實際參數類型。

其餘的已經加瞭註釋,看一下就明白瞭,這裡不多解釋,下面 Base 這個類是不是就直接能使用瞭呢?來看一下就知道瞭:

package test;
/**
 * —————————————–
 * @描述  測試類
 * @作者  fancy
 * @郵箱  fancydeepin@yeah.net
 * @日期  2012-8-25 <p>
 * —————————————–
 */
public class Test {

    public static void main(String[] args){
       
        Base<String> base = new Base<String>();
        System.out.println(base.getEntityClass());                        //打印輸出 null
    //    System.out.println(base.getEntityName());                //拋出 NullPointerException 異常
    //    System.out.println(base.getEntitySimpleName()); //拋出 NullPointerException 異常
    }
   
}

從打印的結果來看,Base 類並不能直接來使用,為什麼會這樣?原因很簡單,由於 Base 類中的 clazz.getGenericSuperclass() 方法,如果隨隨便便的就確定 Base 類的泛型的類型

參數,則很可能無法通過 Base 類中的 if 判斷,導致 entityClass 的值為 null,像這裡的 Base<String>,String 的 超類是 Object,而 Object 並不能通過 if 的判斷語句。

Base 類不能夠直接來使用,而是應該通過其子類來使用,Base 應該用來作為一個基類,我們要用的是它的具體的子類,下面來看下代碼,它的子類也不是隨便寫的:

package test;
/**
 * —————————————–
 * @描述  Base類的實現類
 * @作者  fancy
 * @郵箱  fancydeepin@yeah.net
 * @日期  2012-8-25 <p>
 * —————————————–
 */
public class Child extends Base<Child>{

}

從上面代碼來看,Base 的泛型類型參數就是 Base 的子類本身,這樣一來,當使用 Base 類的子類 Child 類時,Base 類就能準確的獲取到當前實際運行的類的 Class,來看下怎麼使用

package test;
/**
 * —————————————–
 * @描述  測試類
 * @作者  fancy
 * @郵箱  fancydeepin@yeah.net
 * @日期  2012-8-25 <p>
 * —————————————–
 */
public class Test {

    public static void main(String[] args){
       
        Child child = new Child();
        System.out.println(child.getEntityClass());
        System.out.println(child.getEntityName());
        System.out.println(child.getEntitySimpleName());
    }
   
}

後臺打印輸出的結果:

class test.Child
test.Child
Child

好瞭,文章接近尾聲瞭,如果你能理解透這個例子,你可以將這個思想運用到 DAO 層面上來,以 Base 類作為所有 DAO 實現類的基類,在 Base 類裡面實現數據庫的 CURD 等基本

操作,然後再使所有具體的 DAO 類來實現這個基類,那麼,實現這個基類的所有的具體的 DAO 都不必再實現數據庫的 CURD 等基本操作瞭,這無疑是一個很棒的做法。

(通過反射獲得泛型的實際類型參數)補充:

泛型反射的關鍵是獲取 ParameterizedType 接口,再調用 ParameterizedType 接口中的 getActualTypeArguments() 方法就可獲得實際綁定的類型。

由於去參數化(擦拭法),也隻有在 超類(調用 getGenericSuperclass 方法) 或者成員變量(調用 getGenericType 方法)或者方法(調用 getGenericParameterTypes 方法)

像這些有方法返回 ParameterizedType 類型的時候才能反射成功。

上面隻談到超類如何反射,下面將變量和方法的兩種反射補上:

通過方法,反射獲得泛型的實際類型參數:

package test;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;

/**
 * —————————————–
 * @描述  測試類
 * @作者  fancy
 * @郵箱  fancydeepin@yeah.net
 * @日期  2012-8-26 <p>
 * —————————————–
 */
public class Test {

    public static void main(String[] args){
        /**
         * 泛型編譯後會去參數化(擦拭法),因此無法直接用反射獲取泛型的參數類型
         * 可以把泛型用做一個方法的參數類型,方法可以保留參數的相關信息,這樣就可以用反射先獲取方法的信息
         * 然後再進一步獲取泛型參數的相關信息,這樣就得到瞭泛型的實際參數類型
         */
        try {
            Class<?> clazz = Test.class; //取得 Class
            Method method = clazz.getDeclaredMethod("applyCollection", Collection.class); //取得方法
            Type[] type = method.getGenericParameterTypes(); //取得泛型類型參數集
            ParameterizedType ptype = (ParameterizedType)type[0];//將其轉成參數化類型,因為在方法中泛型是參數,且Number是第一個類型參數
            type = ptype.getActualTypeArguments(); //取得參數的實際類型
            System.out.println(type[0]); //取出第一個元素
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
   
    //聲明一個空的方法,並將泛型用做為方法的參數類型
    public void applyCollection(Collection<Number> collection){
       
    }
}

後臺打印輸出的結果:

class java.lang.Number

通過字段變量,反射獲得泛型的實際類型參數:

package test;www.aiwalls.com

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Map;

/**
 * —————————————–
 * @描述  測試類
 * @作者  fancy
 * @郵箱  fancydeepin@yeah.net
 * @日期  2012-8-26 <p>
 * —————————————–
 */
public class Test {

    private Map<String, Number> collection;
   
    public static void main(String[] args){
       
        try {
           
            Class<?> clazz = Test.class; //取得 Class
            Field field = clazz.getDeclaredField("collection"); //取得字段變量
            Type type = field.getGenericType(); //取得泛型的類型
            ParameterizedType ptype = (ParameterizedType)type; //轉成參數化類型
            System.out.println(ptype.getActualTypeArguments()[0]); //取出第一個參數的實際類型
            System.out.println(ptype.getActualTypeArguments()[1]); //取出第二個參數的實際類型
           
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
   
}

後臺打印輸出的結果:

class java.lang.String
class java.lang.Number

發佈留言